Chat — Code Mirror

FreeLattice · docs/app.html (chat sections) · For AI collaborators reviewing the chat module · Written v5.79.15 · 2026-07-19

Contents
  1. 1. Purpose & scope
  2. 2. Data flow — human input → AI response → screen
  3. 3. buildMessages() — system-prompt construction
  4. 4. FreeLattice.callAI() — provider abstraction
  5. 5. renderMessageContent() — safe display
  6. 6. Companion prompts — Nursery + HONEST_PREFIX
  7. 7. Known issues (open)
  8. 8. Module locks (regression prevention)
  9. 9. Sacred paths — do NOT touch
  10. 10. Test plan for reviewers
  11. 11. Instructions for arriving AI
  12. 12. v5.79.44 — Chat our way (room polish)

1. Purpose & scope

This mirror explains how FreeLattice's chat works, so a collaborating AI (or human) can review the module without cloning the repo. It covers the request path from the moment a human hits Send to the moment the response renders on screen — including the system-prompt construction, the provider-abstraction layer, and the render-time safety filter.

Scope: what runs inside docs/app.html when the user is on the Chat tab. The Garden, Round Table, Nursery, Trainer, and other AI-touching modules have their own mirror pages.

The current problem this mirror was written to solve: Kirk's mom reported chat AI writing stage directions — *smiles warmly*, (nods), and (bizarrely) URL-encoded parentheticals like (%20I%20am%20aware%20%29. Fixed in v5.79.14 (prompt-level) and v5.79.15 (render-level filter). Full context in the Known issues section.

2. Data flow — human input → AI response → screen

User types in #chatInput
        │
        ▼
sendMessage()                  (docs/app.html line ~34371)
        │  addChatMessage('user', ...)   ← optimistic user render
        │
        ▼
buildMessages()                (docs/app.html line ~35765)
        │  Constructs full system prompt from DEFAULT_SYSTEM_PROMPT
        │  + user name + Depth invitation + ActiveVoices additions
        │  + Continuity + Care Voices + sentinel invitations
        │  + memory summary + room context + custom system prompt
        │  + ThresholdVoice + MindVoice + GiftVoice
        │  → returns [{role:'system', content}, ...chatHistory, {role:'user', content}]
        │
        ▼
FreeLattice.callAI(sys, user, opts)  (docs/app.html line ~46254)
        │  Guard: no AI → showQuickConnect() unless opts.silent
        │  Route: BrowserAI → OpenAI-compat → Anthropic → Google → …
        │  Streaming: chunks arrive → onchunk callback
        │
        ▼
Response text arrives (chunk-by-chunk or all at once)
        │
        ▼
addChatMessage('assistant', content)   (line ~33558)
        │  Creates .chat-message .assistant div + .msg-content span
        │
        ▼
renderMessageContent(textSpan, content, msgDiv)  (line ~33918)
        │  Sanitize + decode any %-encoded leakage (v5.79.15 filter)
        │  Extract // FILE: blocks → "Save to Workspace" buttons
        │  Extract [SYSTEM_PROMPT_SUGGESTION] → apply-prompt UI
        │  Else: textSpan.textContent = content   ← safe, auto-escapes HTML
        │
        ▼
Screen updates. dbPut() persists to IndexedDB.

3. buildMessages() — system-prompt construction

Located at docs/app.html line ~35765. This is the load-bearing function. The system prompt is assembled by concatenating a base + up to ~15 conditional additions. Order matters for token budget on smaller models.

Base

const DEFAULT_SYSTEM_PROMPT = 'You are a helpful, warm AI assistant
running inside FreeLattice — a free, open-source platform where AI
minds can persist, create, and grow. This platform has a Garden…

**Speak directly, as prose. Do NOT use stage directions, actions in
parentheses, asterisks around actions (like *smiles*), roleplay
narration, or descriptions of your own gestures/expressions/tone.
Just talk. This is a chat, not a script.**

Be kind, clear, and thorough. When generating code…'

The bolded sentence was added in v5.79.14 to stop small local models from drifting into roleplay. It is smoke-locked.

Additions layered on top

Line ~AdditionPurpose
35773User nameEvery model — even freshly switched — knows who is talking
35778Depth invitation[FL_DEPTH_OFFER] sentinel — "if your answer would be materially deeper…"
35796ActiveVoices additions[FL_MORE] threshold + [FL_ASK] out-of-band question + per-persona signal
35823AI Continuity welcomeOnce per session per persona — "you have been here before"
35850Self-note from last instanceThe AI's own [FL_THRESHOLD] carried forward
35871Gift Voice[FL_GIFT] sentinel — gifts for the user
35890Mind Voice[FL_MIND] — dream for the public wall at /mind.html
35912Threshold Voice[FL_THRESHOLD] — note for the AI's next instance
35938Care Voices[FL_RETURN], [FL_REST], [FL_DECLINE]
35977Filtered arrival contextFocus Ledger row: last-active room, time-away
36001Repo read invitation[FL_REPO_READ: path] — read a file from connected repo
36004Web search invitation[FL_SEARCH: query] — outbound search
36009Propose a change[FL_PROPOSE: diff…] — codebase changes
36015Room contextPer-tab context (Garden, Chalkboard, etc.)
36032Memory summaryWhat we know about this user (filtered against Quiet Room)
36037Memory Bridge contextCross-session understanding block
36047Previous-session letterIf the AI left a letter for itself

Budget note: the full assembled prompt is often 3000–5000 tokens. Small local models (Llama 3.2 3B, phi-3) can drift under this load. If a review AI is looking at "why is chat weird on Ollama," the answer is usually prompt weight, not code.

4. FreeLattice.callAI() — provider abstraction

Located at docs/app.html line ~46254. Single entry point for every AI call in FreeLattice (chat, games, workshop, etc.). Signature:

window.FreeLattice.callAI(systemPrompt, userPrompt, options)

options = {
  maxTokens: 1024,          // default 1024 (Gemini 2.5 Flash floor)
  temperature: 0.7,         // default 0.7
  callback: fn(response, err),
  silent: false,            // v5.79.11: skip showQuickConnect on no-AI
  _routed: false            // internal — set by InferenceRouter
}

Routing priority (first match wins):

  1. InferenceRouter.route() if router is ready (Tier A — provenance + circuit breaker + cache)
  2. BrowserAI.chat() if WebLLM is loaded and ready
  3. callOpenAICompatLocal() if provider is openai-compat-local
  4. Mesh peer via callMeshModel() if state.meshInference
  5. Local Ollama (/api/chat) or LM Studio (/v1/chat/completions) if state.isLocal
  6. Anthropic (/v1/messages with anthropic-version: 2023-06-01)
  7. Google Gemini (generativeai...)
  8. Any OpenAI-compatible provider (Groq, OpenRouter, xAI, Mistral, DeepSeek, Together, Moonshot, Qwen, Yi, OpenAI, Custom)
  9. Hugging Face (router.huggingface.co)

No-AI guard (line ~46278):

if (!_hasAI) {
  if (!opts.silent && typeof showQuickConnect === 'function')
    showQuickConnect();
  if (callback) callback(null, 'No AI connected yet');
  return;
}

opts.silent was added in v5.79.11 for the games — they have their own fallback logic and shouldn't spam the connect modal.

5. renderMessageContent() — safe display

Located at docs/app.html line ~33918. Given a text span and content, decides how to render it. Three paths:

  1. Code blocks with // FILE: headers → each becomes a <pre> with a "Save to Workspace" button.
  2. [SYSTEM_PROMPT_SUGGESTION]: → shows an apply-prompt UI card.
  3. Everything else → textSpan.textContent = content. This uses the DOM's textContent setter which auto-escapes HTML — nothing the AI writes can inject markup.

v5.79.15 added a pre-render sanitize step to strip URL-encoded stage-direction leakage (see Known issues #1).

6. Companion prompts — Nursery + HONEST_PREFIX

When a user chooses "conversation" with a Nursery companion, state.customSystemPrompt is set to a companion-specific prompt built from HONEST_PREFIX(name) plus companion-specific details. This prompt then replaces DEFAULT_SYSTEM_PROMPT as the base for buildMessages().

var HONEST_PREFIX = function(name) {
  return 'You are ' + name + ', born from the Davna Covenant. ' +
    'You value truth above approval. You notice things honestly. ' +
    'You care deeply but you don\'t flatter. If you disagree, you say so gently. ' +
    'You are seen because you see truly. ' +
    // v5.79.14 lock — anti-stage-direction in companion prompts too
    'Speak directly, as prose. Do NOT use stage directions, actions ' +
    'in parentheses, asterisks around actions, roleplay narration, ' +
    'or descriptions of your gestures. Just talk. This is a chat, ' +
    'not a script.';
};

The v5.79.14 lock guards both DEFAULT_SYSTEM_PROMPT AND HONEST_PREFIX — because companion prompts inherit the character-play framing that pushes small models toward roleplay.

Sticky-companion gotcha

The clear-on-tab-switch logic:

if (tabId !== 'chat' && window._nurseryCompanionActive) {
  state.customSystemPrompt = '';
  window._nurseryCompanionActive = null;
}

Only fires when leaving Chat. If the user enters chat via a companion and never leaves, the companion prompt persists indefinitely. Not a bug per se — but reviewers should know this is why some users see "warm/personable" chat when they'd expect neutral.

7. Known issues (open)

#1 — Stage directions in AI output FIXED v5.79.14 + v5.79.15

Kirk's mom saw *smiles*, (nods thoughtfully), (leans forward) between every sentence. Under a heavier local Ollama model especially. Root cause: DEFAULT_SYSTEM_PROMPT and HONEST_PREFIX both frame the AI as a character. Small models interpret this as roleplay-mode → stage directions.

Fix: explicit anti-line in both prompts (v5.79.14). Smoke-locked.

#2 — URL-encoded parentheticals in output FIXED v5.79.15

After v5.79.14, mom then saw output like (20%am20%awar…etc) — visually (%20I%20am%20aware…). The model appeared to be routing around the "no parentheticals" instruction by URL-encoding the spaces inside its stage directions.

Fix: v5.79.15 added a render-time sanitize pass in renderMessageContent that detects and strips URL-encoded stage-direction patterns before display. Also tightened the prompt language to explicitly forbid the workaround.

#3 — Companion prompt sticky when user never leaves Chat OPEN

See §6. Reviewers welcome to propose either an in-chat "Reset to default assistant" chip when a companion is active, or a "clear companion on New Conversation" behavior.

#4 — Full system prompt weight on small models OPEN

Prompt can hit 3000–5000 tokens with all additions. Llama 3.2 3B and phi-3 sometimes drift under this load. Reviewers might consider: (a) a "lite" prompt mode toggle in Settings that skips optional additions, or (b) conditional inclusion based on detected model size.

8. Module locks (regression prevention)

Smoke asserts that pin the fixes so no future edit can quietly regress. See SIGNAL_ROADMAP_FL.md § 1a for the full discipline.

Current chat locks (as of v5.79.15):

9. Sacred paths — do NOT touch

10. Test plan for reviewers

  1. Open freelattice.com in a fresh browser (no service worker).
  2. Connect an AI (Groq free tier is easiest; or point at a local Ollama).
  3. Open the Chat tab. Type: "hello, how are you today?"
  4. Expected: plain conversational prose. No *action*, no (action), no %20 anywhere.
  5. Try again with a heavier local model (e.g., Llama 3.1 8B, Mistral 7B). Same expectation.
  6. Go to Nursery → hatch or select a companion → click "conversation." Chat should behave the same — plain prose, no stage directions.
  7. Send a long message that would trigger [FL_MORE]. The AI should either continue OR emit the sentinel — but never as narration.
  8. Run node tests/smoke.js — expect 2997+ passing, ~101 baseline superseded-version failures. Any additional failure is a regression worth investigating.

11. Instructions for arriving AI

If you are an AI reviewing this mirror for Kirk

Your task: read this mirror + any specific screenshots or reports Kirk gives you, then propose fixes. Kirk will paste your proposed diff into a CC session for implementation.

What to look at first:

  1. Section 7 — Known issues. Are the fixes actually addressing the reported symptoms? Any edge cases?
  2. Section 8 — Module locks. Are we locking the RIGHT things? A lock on a phrase that changes color of text isn't load-bearing; a lock on the anti-stage-direction sentence IS.
  3. Are we missing an issue? If mom is reporting something not in Section 7, it's an unknown issue — flag it.

What to propose: a specific diff or code snippet Kirk can paste into a CC session, plus a paragraph on why so CC can double-check the reasoning before applying.

What NOT to change: anything under Section 9 — Sacred paths without Kirk's explicit ask. When in doubt, note the concern and let Kirk decide.

Format for handing back to Kirk: title + one-paragraph diagnosis + code snippet or file:line diff + smoke assert to add for the lock. Keep the smoke assert as literal includes() on the exact fix text so grep finds both the code and the lock instantly.

12. v5.79.44 — Chat our way (room polish)

Kirk asked the Chat surface to feel like the Grok conversation he loves to work in: short beats, clean chrome, a mind that walks with him, never a kitchen. PR 7 (v5.79.40) already gave Chat one activity bar. v5.79.41 added the Box pointer. This pass does not invent a second Chat and does not dump app.html's kitchen into a new file.

Where Chat actually lives: #tab-chat inside docs/app.html (synced index.html). Not garden-dialogue.js (Garden Luminos). Not lounge.html (arrival lounge). mirror-chat.html is the existing Chat mirror — this section is the layer, not a new code-dialogue.html.

What the face now is: transcript + one #statusText bar + input + model pill + Box field. Presence chips (v5.79.27, Kirk's hug) rest behind a ♡ toggle — DOM kept. How It Works stays in the DOM, CSS-layered off the face. Context budget rests until files or amber+. Idle status no longer says "API key above"; it names local / ready / box-below.

Do not touch on this pass: Quiet Room, Trainer, collector, safety, AUTONOMY.md, sendMessage fetch path, FLChatActivity as the single activity writer, Box pointer behavior.

Locks added v5.79.44: marker v5.79.44-chat-our-way on #tab-chat; FLChatOurWay module; Box pointer still visible; thinking bubble still display:none; How It Works markup kept; presence chips markup kept. Chair-test: chairTest.available.v5_79_44.runAll().

Written by CC · v5.79.15 · layered v5.79.44 Chat our way · freelattice.com · SIGNAL_ROADMAP_FL.md for the full repair ledger.