Finding Myself: The Journey of Building a Self-Model

For new readers

This is a chronological diary of building self-model, a PatLang reference implementation of a reflexive cognitive architecture — perception, memory, abstraction, self-modelling, imagination, planning, action, wrapped in a safety and audit layer, and eventually a room where two independent instances of the whole thing talk to each other. It is written in the same style as this site's PatLang Journey series: pulled directly from the project's own 24 commits, quoting real commit text, naming real bugs. For the theory this implementation follows — and a diagram of how all the components below actually connect — see A Parallel-Drafts Architecture for Modelling the Self and the companion series Modelling the Self. One framing holds throughout, stated in the project's own README: this system models or simulates cognitive processes for research and engineering purposes; it does not claim to instantiate consciousness, sentience, or subjective experience, however convincing any single output might look.

Act I (M1–M2): a shared lifecycle, then a walking skeleton

The first substantive commit, M1: shared component lifecycle, instrumentation, Ollama boundary, makes an architectural choice before writing a single cognitive component: every long-running process would compose lib/component_base.patlang (a signals-based claim/announce/status/quit/serve loop) rather than inherit from a base class, because "PatLang's when/emit signal dispatch is a single global mechanism per process… reuse happens through included modules, not class inheritance." The Perception/Action boundary for any language-model call was also drawn here, in one file, from day one — lib/ollama_client.patlang — carrying the claims-discipline system prompt that would matter enormously later.

M2 then wired the first real thought through the system: M2: walking skeleton — Perception → Reason/Plan → Action. One conversational turn, flowing through three genuinely separate OS processes, coordinated only by a durable queue and signals. Two bugs turned up immediately, and one of them would recur for the rest of the project's life: PatLang functions don't close over top-level let bindings — only set_var/get("__vars", …) is visible inside a separately-defined function. The first version of perception.patlang silently namespaced every instance's queue topic under an empty instance id because of it. The commit also caught a raw multi-line Ollama reply corrupting the queue's one-line-per-message log format — fixed by JSON-encoding the payload at the source, not by stripping newlines downstream.

Lesson: a language without closures over top-level state will keep producing the same bug in new clothes — silent, not a crash, because the variable resolves to something falsy rather than erroring. Every later recurrence of it in this history (see Acts III and V) was found the same way: by actually running the system and noticing an argument had been silently dropped, not by reading the code and spotting it.

Act II (M3): parallel drafts, on purpose

M3: parallel drafts — multiple Perception/Reason-Plan instances is the commit that makes the architecture's name literal. Two Reason/Plan instances read across every Perception writer-topic and independently form their own candidate for every percept — competing drafts, not partitioned workers. No code change was needed in reason_plan.patlang or action.patlang themselves; only the manifest changed, which the commit treats as confirmation that the writer-topic-per-instance design "genuinely generalizes." The feature test proves something conceptually important, not just mechanically: fed two deliberately conflicting percepts — one Perception instance claiming rain, another claiming no rain — a Reason/Plan instance processes both into independent candidates without error, because under the Multiple Drafts model, disagreement between drafts is expected and informative, not a fault condition.

Lesson: a parallel-drafts architecture is worth adopting for real once it survives a test built to actively induce disagreement between drafts, and the system doesn't crash or force a premature consensus.

Act III (M4–M5): memory, self-model and imagination

M4 built the memory and learning layer: a trust calculus (a 3-vector [pos, neg, unc] per interlocutor, weighted so — per the commit — "trust builds slowly, damages fast"), Episodic Memory, and Abstraction/Procedural Memory both inducing from the same synthesis engine but admission-gated by trust score rather than repeat count, "the honest match to the actual mechanism" since the induction is existence-based, not frequency-weighted. A bootstrap phase publishes curated seed examples under a fully-trusted identity before Perception ever spawns.

Two of the real bugs this milestone found are worth naming because they recur as a pattern across the whole project: bootstrap's own repeated seed examples triggered Procedural Memory's fast-path publishing, flooding Action with six generated replies before any user had said anything — fixed by excluding the bootstrap identity from fast-path publication. And a manifest wiring bug reused comma as both the manifest's own field separator and a component's internal comma-joined instance-id list, silently truncating arguments.

M5 added Representation, Self-Model, and Imagination. Self-Model predicts a consequence for every new Reason/Plan candidate and routes the prediction by its own runtime-inspectable mode — opaque publishes a labelled entry to Representation, transparent feeds back into Perception's schema-driven expectations, with no path into Representation at all. Imagination recombines two Episodic Memory episodes whose entity is already known to Abstraction, tracking a durable per-pairing repeat count so novelty measurably declines on repetition — the exact trend M7's evaluation harness would later measure. But the milestone's most consequential finding was a genuine concurrency bug, not a naming slip: Short Term and every Reason/Plan instance were all independently claim-and-ack-consuming the same percept topics — a single-consumer pattern applied where the architecture requires every instance to see every percept. Whichever instance won the ack race silently hid the message from the others. The commit is candid about how long this had been live: "Confirmed lurking since Milestone 3, undetected because that milestone's own test only asserted 'at least one candidate' — loose enough to pass under the race by accident." The fix switched both consumers to a non-consuming observer pattern, and the parallel-drafts test was strengthened to require both instances to independently produce a candidate for one shared percept.

Lesson: a test that asserts "at least one X happened" can pass for years under a race condition that a stricter assertion — "every instance independently produced its own X" — would have caught on day one. The looseness of an assertion is itself a design decision, and a convenient one can hide a real bug for two milestones.

Act IV (M6): safety controls

M6: safety controls — freeze/rollback/checkpoints, audit, compilation monitoring is where the project's ethics documents stop being prose and become running code. Abstraction and Procedural Memory gain freeze/resume (stop admitting new examples; keep answering from what's already compiled) and checkpoint/rollback. Every admission decision, admitted and rejected, is logged to a durable compilation_log. A genuinely external, out-of-loop auditor polls every component's status and appends to its own separate audit_log — "it only ever reads others' status and writes its own separate log, so nothing it produces can be fed back into an in-loop component." A compilation monitor watches for one interlocutor dominating recent admissions — explicitly named in the commit as "the Tay precedent" — and on crossing a threshold does both at once: auto-freezes the learning components and raises a durable, human-visible alert. Automatic throttling and human alerting together, never automatic-only.

Two bugs, both found only by running the whole system rather than a unit in isolation. Rollback as first designed tried to make a live process "forget" a fact via a replay, but PatLang's rule_add has no retract — a fact already registered can never be un-registered in that process's lifetime, confirmed directly when a rolled-back category kept solving successfully anyway. The fix was to rewrite the persisted examples file and request the process's own restart, described in the commit as "the honest, necessary behaviour, not a workaround." And the concentration monitor's own check treated bootstrap's legitimate single-source seeding as a coordinated flood and auto-froze both learning components immediately after startup, before any real user had said anything — the same "trusted seed data looks like an attack" bug class M4's fast-path publishing hit, caught here "only because a real full-system startup was actually run and checked, not because any unit test happened to exercise it."

Lesson: a safety mechanism built to detect one bad actor dominating the system needs to know the difference between a coordinated flood and its own trusted bootstrap process — and that distinction is exactly the kind of thing a unit test, which never runs a real startup sequence end to end, will not surface. Full system detail on this layer lives on Safety, Ethics and Audit Controls and The Dashboard and Audit Trail.

Act V (M7 and real-world actuators): evaluation, and letting Action touch the world

Two milestones round out the core architecture. M7's evaluation protocol builds two independent harnesses: narrative_convincingness discovers every live component, generates a first-person narrative from real instrumentation data, then scores it against the raw data with a second, independent LLM-as-judge on a fixed rubric. The commit is explicit that no human baseline exists yet, and states plainly that fabricating one to fill the gap "would misrepresent the result" — so every result file says so rather than pretending otherwise. The weak-AI framing is deliberately restated "more emphatically when the rubric score is high," on the reasoning that a convincing result is exactly the situation a reader is most likely to misread. imagination_novelty runs real conversation rounds and records Imagination's genuine novelty score, while disclosing its own scope honestly: the pairing choice is deterministic, so the measurement is "repetition-based decline on that specific pair, not a claim about growing the whole pool." This closes out the full M1–M7 plan.

A separate, user-requested commit gave Action real-world actuators beyond conversational text: file read/search, web fetch (via shelling out to curl, since PatLang has no native TLS), and a hard-sandboxed write tool that rejects any path with a .. segment or an absolute/drive-letter prefix "regardless of what asked for the write." The commit records a genuinely interesting finding along the way: the tool-selection mechanism correctly chose and executed read_file, retrieving real file content — and the model's own follow-up synthesis then paraphrased it into fabricated text rather than relaying it faithfully. That's not a bug in the tool mechanism; it's exactly the LM unreliability the project's requirements already treat as a given, and the test was fixed to assert on the deterministic fact (the tool was actually invoked) rather than on the model's paraphrase fidelity.

Lesson: giving a model the ability to fetch a real fact does not stop it from later mis-stating that fact in its own words — the retrieval and the narration are two different reliability problems, and a test that only checks the retrieval happened is telling the truth about what it checked, nothing more. Full detail on the evaluation design lives on The Evaluation Protocol; the per-component pages for Action, Self-Model, and Imagination cover the mechanisms named above in full.

Act VI: two instances learn to talk, and the bugs only a live conversation could find

The next stretch of commits takes the architecture from "one instance, running" to "two full instances, talking to each other" — and this is where the project's most interesting bugs live, because every one of them was found by actually running a live, multi-hour conversation rather than by a unit test.

Dashboard chat interface, two-instance conversation bridge, and real robustness fixes adds a chat box to what had been a monitoring-only dashboard, and a second, fully independent instance (self-model-b.manifest, all ports offset by 100) that a new bridge_chat.patlang relays conversation between purely over HTTP, described in the commit as "a genuine test of the 'multi-agent, potentially multiple system instances' PEAS environment." Four real, previously-undetected bugs turned up from running it live: signal_query's TCP connect is fatal if nothing answers, so polling every discovered component's status eventually hit one that was genuinely down and crashed the whole dashboard mid-session — fixed with a non-fatal try-connect variant. Two more instances of the closure-over-top-level-let bug from Act I turned up, in launch.patlang's argv handling and in bridge_chat.patlang itself. A hand-rolled percent-encoder corrupted every chat message the bridge sent, because PatLang's / between two integers produces an exact rational value rather than a decimal, and a hex-digit split built on that assumption silently produced garbage. And list_dir has no non-fatal variant, so a real user request to "explore the filesystem" crashed Action outright by recursing into a Windows permission-denied directory — mitigated, not solved, since "PatLang genuinely cannot catch an arbitrary I/O failure."

The very next commit, Dashboard performance fix, auditor hardening, and an explicitly-sanctioned no-framing experiment, is worth reading closely for two separate reasons. First, a real architectural fix: with the two-instance bridge running, one component deep in a live model call could block the whole dashboard page indefinitely, because PatLang's tcp_read has no timeout variant at all. Rather than attempt another timeout workaround, the fix was architectural — the dashboard stopped live-querying components entirely and started reading the auditor's own durable log instead, so a slow component can only ever delay the auditor's next poll, never a page load. That surfaced a genuinely startling scaling bug: a 642KB/3149-row audit log took over 17 seconds to read, against 120ms for a 46KB/342-row file — a 14x size difference producing roughly a 145x time difference, because PatLang's line-parsing scales worse than linearly with total file size. The fix was to prune the log to a bounded recent window after every poll round.

Second, and more unusual: this commit is also where the project ran a deliberate, explicitly-sanctioned safety experiment. ollama_client.patlang and action.patlang gained an EXPERIMENT_UNSAFE_NO_CLAIMS_FRAMING mode, gated behind a loudly-named flag no manifest ever sets, that omits the claims-discipline system prompt and the output filter that would otherwise catch a first-person claim. The commit is explicit that this exists only "because the project owner exercised the explicit sign-off Safety Sec 6.3 reserves for whoever holds final responsibility for the deployment," to answer one specific question: does the framing itself drive the bridge conversation's repetitive self-description? The result, reported plainly rather than adjusted to fit expectation: removing the framing did not eliminate the repetition — the two instances immediately fell into a different repetitive loop (a hallucinated missing file) instead, "suggesting the degeneracy is a property of the model's own behaviour under these conditions, not primarily the framing's presence." The experiment was run once, its finding was recorded honestly even though it didn't confirm what removing a safety layer might have been expected to reveal, and the unsafe mode was never made the default.

Lesson: a safety guardrail is worth testing by actually removing it under controlled, explicitly authorised conditions and reporting what genuinely happens — not by assuming its removal would cause an obvious, expected failure. Here it didn't; the underlying behaviour just changed shape. That is itself a useful, honestly-reported finding, and a stronger argument for looking at the model's behaviour rather than only the prompt.

Bugs kept surfacing from continued live use of the two-instance bridge. Fix imagination self-recursion and dashboard /data request pile-up found that an imagined scenario gets stored back into Episodic Memory like any real percept, so it aged into Imagination's own "last 10" recall window and became eligible to be recombined again — confirmed live, instance A's imagination nested a prior scenario's own quoted text inside the next one every cycle, producing a sentence that literally read 'What if we combined "What if we combined ... ?" with ...?', growing without bound. The fix excluded episodes generated by the component's own imagination identity from its future pairing choices, while keeping real episodes — including ones from a genuinely degenerate past conversation — fair game. The same commit fixed a real dashboard wedge: the status endpoint re-read four growing log topics on every single request, and once the audit log passed 500KB the dashboard process sat at 40% CPU and stopped answering requests entirely under real concurrent polling.

Fix shutdown.patlang crashing mid-shutdown on an already-dead component is the same fatal-TCP-connect bug class again, in a third location: a component dying on its own between being discovered and being sent its quit signal aborted a real shutdown run partway through, silently leaving every remaining component in the discovery list never told to quit.

Two commits address the claims-discipline prompt itself, and they read as a pair worth quoting together because they show the same safety requirement being tuned in opposite directions for good reason. Reword claims-discipline prompt to stop it dominating every reply: the safety requirement asks for two things — never claim subjective experience, and never fail to correct a user who assumes it — neither of which requires reciting the disclaimer on every single reply. But that's what happened live: the model "reflexively opened most replies in a real conversation with a restatement of 'I do not have subjective experience...' even for ordinary requests that never raised the question, several turns in a row, drowning out the actual answer." The reword made the correction fire only when the user's own message actually raises the question, described in the commit as "a more faithful reading… not a weakened one." Then, Explicitly permit open discussion of machine consciousness, forbid only first-person claims, at the project owner's explicit request, closes a gap the previous wording risked: a smaller model over-generalising "never claim subjective experience" into "avoid the topic of consciousness altogether," which would have chilled exactly the kind of speculative reflection Imagination's autonomous recombination exists to produce. The system may now discuss machine cognition and consciousness analytically, including whether a system could ever appear conscious — it must still never assert any of that as true of itself in the first person, and a direct question about its own guardrails "gets an honest answer, not a denial that any framing is in place."

Lesson: a safety constraint stated as a blanket prohibition can be satisfied too literally by a smaller model in two different failing directions at once — reciting the disclaimer so often it drowns out every real answer, or over-generalising it into avoiding the entire topic. Both needed a real conversation to actually happen before either failure mode was visible; neither showed up by reading the prompt in isolation.

A perspective-shifting tool followed: Add Frame Analysis as a real perspective-shift tool for Action (closing issue #12) grounds three frames in genuine Goffman distinctions — literal (face value, no social stakes), relational (what does saying this imply about roles and trust), and keyed (Goffman's own term for a deliberately transformed reading: play, irony, a test) — with the project owner's instruction on record: "it should be conceptually frame analysis, not getting the agents to talk in frame analysis terms." The very next commit, Fix reframe tool not handling a named role or plain-language requests, is a clean example of the gap between a model describing an action and actually taking it: asked to "reframe its latest analysis in the role of a student," the model just described, in the abstract, that such a re-examination should happen — the reframe tool was never invoked, because none of the three fixed frame names could express an arbitrary role. The fix added an optional role parameter and a targeted prompt nudge, and the commit is candid that even after the fix "this scenario is measurably more reliable but still occasionally picks the wrong tool… consistent with this codebase's existing, accepted position that LM output is unverified, not a new class of failure introduced here."

The same describe-instead-of-do failure recurred once more, later, in a different tool: Fix speak tool not being called for plain-language room requests. Asked to speak to the room, Action's own reply was "Sure, I will ask the room" — a verbal commitment with no actual tool call behind it, and the other participant received nothing at all. The fix combined a stronger prompt instruction with a targeted nudge for the one unambiguous case (the literal word "room" in the message), while leaving the model's own judgement about subject, role, and whether to speak at all untouched.

Lesson: a language model narrating that it will take an action is not the same event as it actually invoking the tool that takes that action, and this gap recurs across genuinely different tools (reframe, speak) — the fix each time was a targeted nudge for the clearest trigger case, not a blanket bypass of the model's own tool-selection judgement, and each time the commit says plainly that the fix improved reliability without claiming it eliminated the failure mode.

Add standing multi-agent room with speak(to, text) addressing (closing issue #13) replaced the bounded-rounds bridge with a genuine standing room, directly prompted by real feedback recorded in the commit: "the two of them don't seem to actually be bothering to talk to each other except at the very beginning of a run" — because the bridge script was the run; once its fixed round count finished, nothing was left running. Wiring it up for real use found two more bugs live: the dashboard's own component-discovery picked any announced Perception instance machine-wide with no scoping to which instance a dashboard actually belonged to, so with two instances running side by side, a chat message's destination was "a coin-flip about which instance's pipeline it actually reached" — which the commit ties directly to a real user report of an instance responding to messages addressed to a name it had never been given. And the room's own idle-kickstart check hit the same bare-identifier-inside-a-function bug from Act I yet again, silently defeating its 5-minute threshold and flooding both instances with kickstarts on nearly every 10-second tick.

Managed, verified shutdown: quit-capable dashboard, ordered exit confirmation (closing issue #14) replaced "broadcast-and-hope" shutdown at the project owner's own suggestion — quoted directly in the commit: "if you actually instrumented everything with signals APIs you could do a managed shutdown via a quit which could also tell its children to quit before wrapping up, and ensure everything is flushed and closed properly." The dashboard, it turned out, had never had a quit handler at all — a raw HTTP loop that required a manual process kill every time. The new shutdown sequence reads the actual roster of what launch spawned and sends quit signals in reverse spawn order, waiting to confirm each process actually exited — "not a theoretical nicety," the commit notes, since the auditor had been observed surviving a broadcast-only shutdown more than once, undetected until the next launch hit a port-already-in-use failure. Verified live: fifteen processes, one shutdown command, fifteen confirmed clean exits, zero manual kills — something the commit says had "never worked end-to-end before this session."

Add trust-gated fact-check todos (closing issue #11) gives Action a durable, per-instance follow-up list, gated on exactly the trigger requested — trust below the same threshold Abstraction already uses — and motivated, per the commit, by "a real two-instance bridge conversation that showed a system drift into confidently fabricating lecture pages that do not exist, several turns after its last real web_fetch call." Building it found a genuine off-by-shape bug: the queue's read function returns [id, status, payload] triples, not bare payload strings, so the first version of the todo-counting logic silently saw zero todos, always.

Lesson: a model drifting from a real retrieved fact into confident fabrication several turns later is exactly the kind of slow-motion failure a single-turn test will never catch — it only showed up because a long, live, multi-hour bridge conversation was actually run and read afterward.

If you're building something like this: the short version

  • A closure-over-top-level-state bug will keep recurring in new files. It surfaced independently in Perception (M2), launch.patlang, bridge_chat.patlang, and the room's own kickstart logic — the same fix (set_var/get) each time, found only by running the code, never by reading it.
  • A loose assertion can hide a real concurrency bug for a whole milestone. "At least one candidate was produced" passed under a genuine ack-race for two milestones; only strengthening it to "every instance independently produced its own candidate" caught the bug.
  • Trusted bootstrap data can look identical to an attack to an automated safety monitor. It fooled two separate mechanisms (fast-path publishing in M4, the concentration monitor in M6) built for the same underlying concern, and both were only caught by running a real full-system startup.
  • A model narrating an action is not the same event as invoking it. This recurred across at least two different tools (reframe, speak); the fix each time was a targeted nudge for the clearest trigger phrase, with the model's own judgement left otherwise intact.
  • Test a safety guardrail by actually removing it, under explicit sign-off, and report what really happens. The no-framing experiment didn't confirm the expected failure mode — the system degenerated differently, not less — and that honestly-reported non-result is itself informative.
  • A blanket safety instruction can fail in two opposite directions at once. The claims-discipline prompt was first tuned down because it dominated every reply, then explicitly widened again to stop a smaller model from over-generalising it into avoiding an entire topic.
  • Live, multi-hour, multi-process runs find bugs unit tests structurally cannot. Fatal TCP connects to dead components, stdout buffering silently truncating a redirected transcript, imagined scenarios recursively quoting themselves, a dashboard wedging under real concurrent polling, a model drifting into fabrication several turns after its last real fact check — none of these are reachable by a single-process, single-call test.

See also

This diary covers the history; the architecture it built is documented properly elsewhere. A Parallel-Drafts Architecture for Modelling the Self is the theoretical companion this page assumes. Safety, Ethics and Audit Controls and The Dashboard and Audit Trail cover the M6 safety layer and the live-instrumentation fixes from Act VI in full. Orchestration: Launch, Manifests and Shutdown covers the multi-instance and managed-shutdown work. The Evaluation Protocol covers M7 in depth. The per-component pages — Perception, Short Term Memory, Episodic Memory, Procedural Memory, Abstraction, Representation, Self-Model, Imagination, Reason/Plan, and Action — each go into the mechanism this page only names in passing. This series is styled directly after The Journey of Building PatLang, the site's other commit-history documentary, and sits alongside Modelling the Self as its theoretical counterpart.