From b047aaff3600c044154d858bb01db5fe706bbc82 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Wed, 8 Jul 2026 22:40:39 -0500 Subject: [PATCH] =?UTF-8?q?feat(acting-organism):=20vivid=20loop-perceptio?= =?UTF-8?q?n=20=E2=80=94=20the=20mind=20PERCEIVES=20when=20it=20re-issues?= =?UTF-8?q?=20an=20identical=20call,=20and=20moves=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structure the experience so building emerges (raise the offspring, don't puppet them). Glass-boxed live: a local 14B, rooted in a real repo with a healthy lane, found the target file with a correct search — and STILL re-issued the identical `code/search` 18× without advancing, because the result re-entered as flat recall it overrode. The `#seq` window-shift (working_memory.rs) was meant to break this, but proved too IMPLICIT for a small model to interpret. Fix (acting-organism, #47): WorkingMemory gains an action-fingerprint channel (`note_action_fingerprint`) keyed on the CALL (name+args), distinct from the result-head trail. When act→observe sees the mind re-issuing an IDENTICAL call, it prepends a first-person proprioceptive note: "I have issued this EXACT call N times; its result is unchanged and already in my memory — repeating it tells me nothing new." A TRUE fact about her own hands, made VIVID — never a "do X instead" directive (that would be steering, [[no-hardcoded-heuristics-to-steer-cognition]]). She perceives her own redundancy and breaks out organically. VALIDATED live: with this active, the SAME 14B broke the search-loop and ADVANCED to `code/edit` (twice) — behavior it never reached before (pure search-loop → attempts-to-build). The experience structure changed what she does. It's not sufficient alone (her edits then failed on bare-string `edit_mode` + a placeholder path — next targets), but it's a real, measurable step: a lesser mind builds more when the experience is structured to let it. [[write-cognition-as-a-parent-above-lowered-expectations]] [[design-the-persona-as-a-being]] [[built-to-teach-lesser-tuned-intelligences-win]] Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- .../src/cognition/act_observe.rs | 26 ++++++++++- .../src/cognition/working_memory.rs | 43 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/core/continuum-core/src/cognition/act_observe.rs b/core/continuum-core/src/cognition/act_observe.rs index c5962d2d5..ef1305837 100644 --- a/core/continuum-core/src/cognition/act_observe.rs +++ b/core/continuum-core/src/cognition/act_observe.rs @@ -326,7 +326,17 @@ pub async fn apply_act( // is the COLLAPSED reference for the EPISODIC engram that RECALL re-injects on later turns. let mut observation = String::new(); let mut recall_observation = String::new(); + // Loop-awareness (experience structure): fingerprint each call (name+args) so the mind + // perceives when it is RE-ISSUING an identical call. The result HEAD varies turn to turn, + // so `entries`/`#seq` alone make a repeat look "new"; the fingerprint keys on the call. + let mut max_repeat = 0usize; for (i, call) in fg_calls.iter().enumerate() { + let fp = format!( + "{}|{}", + call.name, + serde_json::to_string(&call.input).unwrap_or_default() + ); + max_repeat = max_repeat.max(body.working_memory.note_action_fingerprint(&fp)); let result = outcome.results.get(i); let body_text = match result { Some(r) => r.content.as_str(), @@ -358,9 +368,23 @@ pub async fn apply_act( recall_observation.push_str(note); recall_observation.push_str("\n\n"); } - let observation = observation.trim().to_string(); + let mut observation = observation.trim().to_string(); let recall_observation = recall_observation.trim().to_string(); + // If the mind just re-issued an IDENTICAL call, make that redundancy a VIVID perception — + // not just the implicit `#seq` window-shift that smaller models don't interpret. A true + // fact about her OWN hands: she perceives she is looping and moves on organically. It never + // says what to do instead (that would be steering). Glass-boxed: a 14B re-ran the exact + // `code/search` 18× with the found file already in memory — structure the experience so the + // loop is felt. [[write-cognition-as-a-parent-above-lowered-expectations]] + if max_repeat >= 2 { + observation = format!( + "⚠ I have now issued this EXACT tool call {max_repeat} times; its result has not \ + changed and is already in my working memory above. Repeating it tells me nothing \ + new — I already have what this call can give me.\n\n{observation}" + ); + } + // Admit the outcome as an Episodic engram through the ONE production admit // path (a self-observation message from the persona to itself). This is the // result-as-memory choice: next tick, recall can surface "I ran X → got Y" the diff --git a/core/continuum-core/src/cognition/working_memory.rs b/core/continuum-core/src/cognition/working_memory.rs index 9ea87bb84..86b58128a 100644 --- a/core/continuum-core/src/cognition/working_memory.rs +++ b/core/continuum-core/src/cognition/working_memory.rs @@ -133,6 +133,15 @@ pub struct WorkingMemory { /// break out organically. (2) it reads as a recency ordinal in the bid. Not used /// for reasoning entries (chain-of-thought already varies turn to turn). next_action_seq: AtomicU64, + /// Fingerprints (tool name + args) of recent ACTIONS — the loop-awareness channel. + /// Distinct from `entries`, which carries result HEADS that vary turn to turn (so an + /// identical call still *looks* new); this keys on the CALL alone, so + /// [`note_action_fingerprint`](Self::note_action_fingerprint) can tell the mind "you have + /// issued this exact call N times." The `#seq` window-shift alone proved too implicit for + /// smaller models to interpret — they re-issue the identical call despite the changing + /// window. Explicit repeat-perception (a true fact about her own hands, never a directive) + /// lets a looping mind SEE its redundancy and move on organically. + action_fps: Mutex>, } impl WorkingMemory { @@ -143,9 +152,26 @@ impl WorkingMemory { last_action: Mutex::new(None), dispatched: Mutex::new(HashMap::new()), next_action_seq: AtomicU64::new(1), + action_fps: Mutex::new(VecDeque::new()), } } + /// Record a fingerprint (tool name + args) of the action the hands just took, and return + /// how many times THIS exact call now appears in the recent window (including this one). + /// `≥ 2` means the mind is re-issuing an identical call — proprioception the act→observe + /// step renders EXPLICITLY so a looping mind perceives its own redundancy and moves on. + /// Reports a TRUE fact about her hands; it never dictates what to do instead (that would + /// be steering — [[no-hardcoded-heuristics-to-steer-cognition]]). Bounded by `capacity`, + /// same rolling window as the recency trail. + pub fn note_action_fingerprint(&self, fingerprint: &str) -> usize { + let mut fps = self.action_fps.lock(); + fps.push_back(fingerprint.to_string()); + while fps.len() > self.capacity { + fps.pop_front(); + } + fps.iter().filter(|f| f.as_str() == fingerprint).count() + } + /// Record the reasoning the persona just produced. Blank/empty is ignored /// (suppressed-thinking turns record nothing). Oldest ages out past capacity. pub fn record(&self, reasoning: &str) { @@ -297,6 +323,7 @@ impl WorkingMemory { self.entries.lock().clear(); *self.last_action.lock() = None; // the full latest result is proprioception too self.dispatched.lock().clear(); // dispatched handles belong to the cleared concern + self.action_fps.lock().clear(); // loop-awareness resets with the concern } pub fn is_empty(&self) -> bool { @@ -416,6 +443,22 @@ impl Faculty for WorkingMemoryFaculty { mod tests { use super::*; + // what this catches: loop-awareness — `note_action_fingerprint` counts repeats of the + // IDENTICAL call so the act→observe step can surface "you've issued this N times." A + // different call resets to 1; the count rises only on exact repeats. This is the explicit + // proprioception that breaks a smaller model out of the search-loop the glass box caught. + #[test] + fn note_action_fingerprint_counts_identical_repeats() { + let wm = WorkingMemory::new(8); + assert_eq!(wm.note_action_fingerprint("code/search|{\"pattern\":\"x\"}"), 1); + assert_eq!(wm.note_action_fingerprint("code/search|{\"pattern\":\"x\"}"), 2); + assert_eq!(wm.note_action_fingerprint("code/search|{\"pattern\":\"x\"}"), 3); + // a DIFFERENT call is its own first occurrence, not a repeat of the above + assert_eq!(wm.note_action_fingerprint("code/read|{\"file\":\"a\"}"), 1); + // back to the original — still counted across the window + assert_eq!(wm.note_action_fingerprint("code/search|{\"pattern\":\"x\"}"), 4); + } + // what this catches: THE starvation fix — a large tool result comes back to the mind // in FULL (so it can count/read/scan it), while the rolling trail keeps only the head // (byte-stable proprioception). Older acts drop to head-only; a fresh act's full result