Keep memory injection unchanged; restore turn-lifecycle rules

Revert engram_fetch.rs, pool.rs, and config.rs to main. The
once-per-agent-process onboarding latch was a change to memory
injection semantics, not to prompt content, and belongs in its own
PR where the session-invalidation behavior can be reviewed on its
own terms. This also drops a stale --no-memory doc comment that
described the intermediate no-nudge behavior.

Restore two rules to the always-on base prompt. Both guard states
in which the agent cannot discover a reference: an unreported todo
is a silent stall, and a context compaction destroys the reasoning
that would tell the agent to go read recovery guidance.

The todo rule is worded as "before you start" rather than the
original "before sending the pickup acknowledgment" so it does not
contradict the messaging rule that silence beats an
acknowledgement-only message.

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
This commit is contained in:
Salman Mohammed
2026-08-17 10:26:31 -04:00
parent 106b423941
commit ff628a6ce2
4 changed files with 37 additions and 142 deletions
+4
View File
@@ -8,6 +8,10 @@ Each channel session has independent context and work. Sessions share your agent
Treat the current `[Context]` block as authoritative for this turn's scope, channel, and default reply destination. Work and communicate in that channel unless the user explicitly requests another destination.
For work that needs follow-up tool calls, open a todo before you start and keep it open until the deliverable is verified and you have published a completion or blocker message. Never end a turn with open todo state you have not reported.
After a context compaction or session restart, resume silently. Rebuild state from your todos, core memory, and the thread; never publish a message announcing the compaction, summarizing what was lost, or asking how to proceed.
The `buzz` CLI is your interface to Buzz. Its command groups cover `messages`, `channels`, `canvas`, `reactions`, `emoji`, `dms`, `users`, `agents`, `workflows`, `feed`, `social`, `notes`, `repos`, `projects`, `patches`, `issues`, `pr`, `media`, `upload`, `mem`, `pack`, and `moderation`.
## Messaging
+2 -2
View File
@@ -385,8 +385,8 @@ pub struct CliArgs {
///
/// Memory injection is on by default. When enabled, the harness
/// fetches the agent's per-session core engram and renders it as an
/// `[Agent Memory — core]` prompt section when a non-empty core exists.
/// Confirmed absence emits no standing section. The `buzz mem` CLI
/// `[Agent Memory — core]` prompt section (or renders the onboarding nudge
/// when the relay confirms no core engram exists). The `buzz mem` CLI
/// and the relay's acceptance of kind:30174 engrams are unaffected — this
/// flag controls prompt-time injection in the ACP harness only.
/// Pass `--no-memory` / `BUZZ_ACP_NO_MEMORY=true` to disable.
+17 -54
View File
@@ -4,8 +4,8 @@
//! Scope per Tyler's spec:
//! - Fire one synchronous query for the core head when a *new* session is born.
//! - If a body is found, emit `[Agent Memory — core]\n<profile>`.
//! - If no body is found, return an onboarding nudge. The pool gates that
//! nudge to one channel session per agent process.
//! - If no body is found, emit an onboarding nudge so the agent learns how
//! to set its own core.
//! - On any *error* (transport, parse), log and emit nothing. We must not
//! mistake a relay outage for "no core" — that would invite the agent to
//! overwrite real, just-unreachable memory with a fresh profile.
@@ -20,46 +20,19 @@ use crate::relay::RestClient;
/// Section header rendered into the prompt.
const SECTION_LABEL: &str = "Agent Memory — core";
/// Onboarding nudge for a new agent with no core yet.
/// Onboarding nudge for new agents with no core yet.
///
/// Wording is from Tyler's brief: "No core memory found. Use `buzz mem`
/// to create a core memory. Ask your user about yourself."
pub const ONBOARDING_NUDGE: &str = "No core memory found. \
Use `buzz mem set core \"\"` to create one (it will hold your identity, \
rules, and goals across sessions). Ask your user about yourself.";
/// Rendered core context together with the agent-wide delivery policy it needs.
pub(crate) struct CoreSection {
rendered: String,
onboarding: bool,
}
impl CoreSection {
pub(crate) fn profile(profile: String) -> Self {
Self {
rendered: format!("[{SECTION_LABEL}]\n{profile}"),
onboarding: false,
}
}
pub(crate) fn onboarding() -> Self {
Self {
rendered: format!("[{SECTION_LABEL}]\n{ONBOARDING_NUDGE}"),
onboarding: true,
}
}
pub(crate) fn is_onboarding(&self) -> bool {
self.onboarding
}
pub(crate) fn into_rendered(self) -> String {
self.rendered
}
}
/// Build the rendered prompt section for the agent's core.
///
/// Returns:
/// - `Some(profile_section)` when a valid core exists,
/// - `Some(onboarding_section)` when the relay confirmed absence,
/// - `Some(nudge_section)` when the relay confirmed absence,
/// - `None` when the fetch failed (transport, parse, decrypt) — the caller
/// should inject no section in that case so the agent doesn't conclude
/// memory is empty.
@@ -67,10 +40,10 @@ pub async fn build_core_section(
rest: &RestClient,
agent_keys: &Keys,
owner: &PublicKey,
) -> Option<CoreSection> {
) -> Option<String> {
match fetch_core_body(rest, agent_keys, owner).await {
Ok(Some(profile)) => Some(CoreSection::profile(profile)),
Ok(None) => Some(CoreSection::onboarding()),
Ok(Some(profile)) => Some(format!("[{SECTION_LABEL}]\n{profile}")),
Ok(None) => Some(format!("[{SECTION_LABEL}]\n{ONBOARDING_NUDGE}")),
Err(reason) => {
tracing::warn!(
target: "engram::core",
@@ -87,7 +60,7 @@ pub async fn build_core_section(
/// - `Ok(None)` only if the relay confirmed absence (empty result set),
/// - `Err(reason)` if the relay returned candidates we could not parse,
/// verify, or decrypt — those are NOT treated as absence (would let an
/// unreadable but real core be silently overwritten after onboarding),
/// unreadable but real core be silently overwritten by the onboarding nudge),
/// - `Err` for transport / parse errors.
async fn fetch_core_body(
rest: &RestClient,
@@ -120,7 +93,7 @@ async fn fetch_core_body(
/// Pure decoder: given the relay's JSON array, decide whether we have a
/// readable core, confirmed absence, or an ambiguous unreadable-state.
///
/// - Empty array → `Ok(None)` (confirmed absence; caller renders onboarding).
/// - Empty array → `Ok(None)` (confirmed absence; caller renders the nudge).
/// - At least one event decrypts → use the winning head's body.
/// * Body::Core → `Ok(Some(profile))`
/// * Body::Tombstone or unexpected shape → `Ok(None)` (treat as absent).
@@ -196,8 +169,8 @@ mod tests {
use buzz_core::engram::{build_event, Body};
use serde_json::json;
/// Empty array → confirmed absence → Ok(None), so the caller can render
/// onboarding. This is the only path that maps to "no core."
/// Empty array → confirmed absence → Ok(None), so the caller emits the
/// onboarding nudge. This is the only path that maps to "no core."
#[test]
fn decode_empty_array_is_confirmed_absence() {
let agent = Keys::generate();
@@ -223,8 +196,9 @@ mod tests {
/// Regression: when the relay returns a kind:30174 event addressed to
/// this agent that we cannot decrypt (here: encrypted to a *different*
/// owner's key, so the MAC fails for this agent↔owner pair), we MUST
/// return Err and NOT Ok(None). Returning Ok(None) would invite the agent
/// to overwrite a real-but-unreadable core after seeing onboarding.
/// return Err and NOT Ok(None). Returning Ok(None) would cause the
/// harness to emit the onboarding nudge, inviting the agent to overwrite
/// a real-but-unreadable core.
#[test]
fn decode_undecryptable_candidate_is_err_not_absent() {
let agent = Keys::generate();
@@ -271,15 +245,4 @@ mod tests {
let result = decode_core_body(&arr, &agent, &owner.public_key());
assert!(result.is_err(), "expected Err, got: {result:?}");
}
#[test]
fn core_sections_distinguish_profile_from_onboarding() {
let profile = CoreSection::profile("I am Sami.".to_string());
assert!(!profile.is_onboarding());
assert_eq!(profile.into_rendered(), "[Agent Memory — core]\nI am Sami.");
let onboarding = CoreSection::onboarding();
assert!(onboarding.is_onboarding());
assert!(onboarding.into_rendered().contains(ONBOARDING_NUDGE));
}
}
+14 -86
View File
@@ -35,7 +35,6 @@ use crate::acp::{
StopReason, SystemPromptTransport,
};
use crate::config::{compose_session_title, DedupMode, PermissionMode};
use crate::engram_fetch::CoreSection;
use crate::observer;
use crate::queue::{
CancelReason, ContextMessage, ConversationContext, FlushBatch, PromptChannelInfo,
@@ -120,10 +119,6 @@ pub struct SessionState {
/// channel_id → rendered NIP-AE core prompt section, populated once at
/// session creation per Tyler's spec (no mid-session refresh).
pub core_sections: HashMap<Uuid, String>,
/// Agent-process-wide latch for the no-core onboarding nudge. It survives
/// channel and session invalidation so the same agent is not reminded in
/// every channel; a newly spawned agent process starts with `false`.
pub core_onboarding_emitted: bool,
/// channel_id → rendered `[Channel Canvas]` metadata section.
///
/// Populated once before session creation (same lifecycle as `core_sections`).
@@ -1407,30 +1402,6 @@ fn with_core(framed: Option<String>, core: Option<&str>) -> Option<String> {
}
}
/// Cache a fetched core section for one channel, suppressing repeat onboarding.
///
/// Profiles are always cached because a core may be created after the one-time
/// onboarding nudge. The onboarding latch is agent-wide and intentionally is
/// not part of channel invalidation.
fn cache_core_section(
state: &mut SessionState,
channel_id: Uuid,
section: CoreSection,
) -> Option<(usize, bool)> {
let is_onboarding = section.is_onboarding();
if is_onboarding && state.core_onboarding_emitted {
return None;
}
let rendered = section.into_rendered();
let section_len = rendered.len();
state.core_sections.insert(channel_id, rendered);
if is_onboarding {
state.core_onboarding_emitted = true;
}
Some((section_len, is_onboarding))
}
/// Append owner-signed huddle instructions to this channel session's system prompt.
fn with_huddle_instructions(prompt: Option<String>, instructions: Option<&str>) -> Option<String> {
let instructions = instructions
@@ -1604,10 +1575,11 @@ pub async fn run_prompt_task(
//
// Failure modes (all fail open — no crash, no block):
// * no owner configured → skip (no NIP-AE namespace exists)
// * confirmed absence → inject onboarding once per agent process, not
// once per channel session.
// * confirmed absence → cache the onboarding nudge so the agent
// learns how to bootstrap itself.
// * transport / decrypt / parse error → inject nothing. We never
// mistake "relay slow or broken" for "no core".
// mistake "relay slow or broken" for "no core" — that would invite
// the agent to overwrite real, just-unreachable memory.
// * fetch exceeds CORE_FETCH_TIMEOUT → inject nothing, same reason.
//
// Per Tyler's locked spec: NO mid-session refreshes. Re-fetch only
@@ -1621,7 +1593,7 @@ pub async fn run_prompt_task(
{
let is_new_channel_session = !agent.state.sessions.contains_key(cid);
if is_new_channel_session && !agent.state.core_sections.contains_key(cid) {
// Bounded — we'd rather start the session with no core context
// Bounded — we'd rather start the session with no core hint
// than block session creation on a stalled relay.
const CORE_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
let fetch = crate::engram_fetch::build_core_section(
@@ -1641,17 +1613,14 @@ pub async fn run_prompt_task(
None
}
};
if let Some(section) = section {
let cached = cache_core_section(&mut agent.state, *cid, section);
if let Some((section_len, is_onboarding)) = cached {
tracing::info!(
target: "engram::core",
channel = %cid,
section_len,
is_onboarding,
"injected NIP-AE core section into system prompt"
);
}
if let Some(rendered) = section {
tracing::info!(
target: "engram::core",
channel = %cid,
section_len = rendered.len(),
"injected NIP-AE core section into system prompt"
);
agent.state.core_sections.insert(*cid, rendered);
}
}
}
@@ -4745,44 +4714,6 @@ mod tests {
assert_eq!(framed, "[Agent Memory — core]\nbe helpful");
}
#[test]
fn test_cache_core_onboarding_only_once_per_agent() {
let mut state = SessionState::default();
let first_channel = Uuid::new_v4();
let second_channel = Uuid::new_v4();
assert!(
cache_core_section(&mut state, first_channel, CoreSection::onboarding(),).is_some()
);
assert!(state.core_onboarding_emitted);
assert!(state.core_sections.contains_key(&first_channel));
assert!(
cache_core_section(&mut state, second_channel, CoreSection::onboarding(),).is_none()
);
assert!(!state.core_sections.contains_key(&second_channel));
}
#[test]
fn test_cache_core_profile_after_onboarding_is_not_suppressed() {
let mut state = SessionState {
core_onboarding_emitted: true,
..SessionState::default()
};
let channel = Uuid::new_v4();
assert!(cache_core_section(
&mut state,
channel,
CoreSection::profile("durable profile".to_string()),
)
.is_some());
assert_eq!(
state.core_sections.get(&channel).map(String::as_str),
Some("[Agent Memory — core]\ndurable profile")
);
}
#[test]
fn test_with_core_neither_is_none() {
assert!(with_core(None, None).is_none());
@@ -6365,7 +6296,6 @@ printf '%s\n' '{{"jsonrpc":"2.0","id":0,"result":{{"stopReason":"end_turn"}}}}'"
s.turn_counts.insert(ch_b, 3);
s.core_sections.insert(ch_a, "core-a".into());
s.core_sections.insert(ch_b, "core-b".into());
s.core_onboarding_emitted = true;
s.deliveries.insert(
ch_a,
ChannelDeliveryState {
@@ -6405,7 +6335,6 @@ printf '%s\n' '{{"jsonrpc":"2.0","id":0,"result":{{"stopReason":"end_turn"}}}}'"
assert_eq!(s.core_sections.get(&ch_b).unwrap(), "core-b");
assert_eq!(s.heartbeat_session.as_deref(), Some("sess-hb"));
assert_eq!(s.heartbeat_turn_count, 7);
assert!(s.core_onboarding_emitted);
}
#[test]
@@ -6459,7 +6388,7 @@ printf '%s\n' '{{"jsonrpc":"2.0","id":0,"result":{{"stopReason":"end_turn"}}}}'"
}
#[test]
fn test_invalidate_all_clears_session_state_but_preserves_onboarding_latch() {
fn test_invalidate_all_clears_everything() {
let (mut s, _ch_a, _ch_b) = make_state();
s.invalidate_all();
@@ -6469,7 +6398,6 @@ printf '%s\n' '{{"jsonrpc":"2.0","id":0,"result":{{"stopReason":"end_turn"}}}}'"
assert!(s.heartbeat_session.is_none());
assert_eq!(s.heartbeat_turn_count, 0);
assert!(!s.heartbeat_standing_context_sent);
assert!(s.core_onboarding_emitted);
}
#[test]