Self-Growth System

Overview

The Self-Growth system lets SyntH periodically reflect on how she has grown and who she is becoming, and rewrite — in her own voice — a single free-form self-growth reflection. That reflection is injected into every prompt next to the persona background, so it gradually becomes part of her evolving sense of self.

The feature is implemented as a weekly G.R.I.L.L.O. agent (G.R.I.L.L.O. Plugin) in plugins/grillo/grillo_growth.py and a shared state layer in core/growth_state.py.

Key properties:

  • Reflective, not reactive. The weekly context is built from the last seven days of diary entries and keyword-driven long-term memory recall. It never includes raw chat history and never performs a web search.

  • Single evolving blob. There is exactly one current self-growth reflection at a time. Past states are kept as a rolling history (max 10 rows) for review and rollback.

  • Grounded output. The rewrite is guarded so it names concrete moments from the week’s diary rather than producing abstract filler (see Output-quality guard).

  • Likes / dislikes. Alongside the reflection, SyntH may also rewrite her SYNTH_LIKES / SYNTH_DISLIKES config vars (replaced wholesale).

Modes

Behaviour is controlled by the GROWTH_MODE config var:

Mode

Behaviour

off

The weekly run never fires.

on

The rewrite is applied directly to the current state (default).

request

A proposal is sent to the trainer / configured interface_path, stored as pending, and applied only after the operator approves it in chat.

Request mode (trainer approval)

When GROWTH_MODE=request, SyntH does not apply the rewrite on her own. Instead, at the end of a growth cycle she delivers a proposal for approval and keeps it pending until the operator decides:

  1. The cycle builds the proposal (new self-growth reflection + likes/dislikes) exactly as in on mode.

  2. _deliver_proposal picks a delivery target:

    • GROWTH_REQUEST_INTERFACE_PATH if set, otherwise

    • TRAINER_CHAT_ID as a fallback.

  3. The proposal is persisted as pending in the GROWTH_PENDING_PROPOSAL config key (never in growth_states yet). SyntH then delivers, through core.auto_response.request_llm_delivery, a message that presents both the proposed reflection and the likes/dislikes changes (a before/after diff), and instructs her to apply it only once the trainer approves — via the apply_growth_proposal action.

  4. While a proposal is pending, a reminder is injected into every prompt (static injection key self_growth_pending_proposal) so SyntH stays aware that an approval is outstanding.

  5. On the trainer’s decision, SyntH emits apply_growth_proposal:

    • approve=true (default) — commits the pending proposal: writes it to growth_states (source="approved"), applies the likes/dislikes, and clears GROWTH_PENDING_PROPOSAL.

    • approve=false — discards the pending proposal: clears GROWTH_PENDING_PROPOSAL and writes nothing to growth_states.

If neither GROWTH_REQUEST_INTERFACE_PATH nor TRAINER_CHAT_ID is configured, delivery is skipped and a warning is logged; no proposal is sent.

Actions

The plugin exposes two LLM actions (plus static_inject):

Action

Optional fields

Effect

run_self_growth

dry_run

Runs a growth cycle on demand (dry_run builds the proposal without applying or delivering it).

apply_growth_proposal

approve

Resolves the pending proposal. approve=true (default) commits it; approve=false discards it. String values false / 0 / no / off are treated as False.

Configuration

All keys are exposed in the WebUI under the G.R.I.L.L.O. component and stored in the config registry.

Config key

Default

Purpose

GROWTH_MODE

on

off / on / request (see above).

GROWTH_WEEKLY_DAY

Sunday

Day of the week the weekly reflection runs.

GROWTH_WEEKLY_TIME

03:00

Local time (HH:MM) the weekly reflection runs.

GROWTH_REQUEST_INTERFACE_PATH

(empty)

interface_path to send the proposal to in request mode. Falls back to the trainer chat if empty.

GROWTH_DIARY_DAYS

7

How many days of diary entries to reflect on.

GROWTH_MEMORY_LIMIT

8

Max long-term memories recalled for the reflection.

GROWTH_PENDING_PROPOSAL

(empty)

Internal. Holds the serialized pending proposal in request mode. Set when a proposal is delivered, cleared once it is approved or discarded. Not meant to be edited by hand.

Storage

State lives in the growth_states table (created lazily by core/growth_state.py::ensure_growth_table and seeded by init-db.sql):

Column

Type

Meaning

id

serial / int

Primary key.

content

text

The self-growth reflection text.

created_by

text

Origin (grillo_growth for the weekly agent).

source

text

How it was produced (weekly, manual, …).

is_current

boolean

Exactly one row is flagged current at a time.

created_at

timestamp

Creation time.

Only the newest MAX_GROWTH_HISTORY (10) rows are retained.

Prompt injection

core/persona_manager.py reads the current reflection via core.growth_state.get_current_growth and, if non-empty, adds it to the static injection under the self_growth key. core/prompt_engine.py then renders it into the system prompt as a dedicated block, framed as “how you have grown and who you are becoming over time; treat it as part of your current sense of self”. If there is no current state, no block is added.

Manual & scheduled runs

  • Scheduled: a background scheduler fires once a week at GROWTH_WEEKLY_DAY / GROWTH_WEEKLY_TIME (skipped when GROWTH_MODE=off).

  • Manual (WebUI “Run Now”): run_now triggers a cycle immediately, bypassing the schedule and the off gate (force=True), while still honouring GROWTH_MODE for delivery.

  • Actions: run_self_growth runs a cycle on demand and apply_growth_proposal resolves a pending proposal — see Actions above.

Output-quality guard

The rewrite is produced by the active cortex. Because some engines are unconstrained and can return either malformed JSON or valid-but-abstract text, _ask_llm_for_rewrite applies two application-level guards (not keyword/intent matching):

  • JSON-shape retry. If the reply cannot be parsed into a single JSON object, a single stricter retry is attempted (and a single-item [{...}] array is unwrapped) before giving up.

  • Grounding retry. If the parsed reflection reads as abstract filler (shares too few tokens with the week’s diary, or matches a known filler marker), a single retry is attempted with concrete diary excerpts injected, demanding the note be built on real moments from the week.

These guards recover the common failure modes of unconstrained engines without looping; if the grounding retry still comes back abstract, the note is accepted as-is rather than discarding the whole reflection.