Appearance
Telegram Dynamic Tailoring -- Phase 1
Design spec for inline keyboard feedback and EMA scoring in the Hinata Telegram bot (CT106 minato-telegram).
Architecture
mermaid
graph TD
subgraph CT106["CT106 minato-telegram"]
BOT["telegram-bot.py"]
SCHED["APScheduler<br/>(EMA cron)"]
end
subgraph CT100["CT100 jimmy-neutron-postgres"]
DB[(PostgreSQL 15<br/>hinata)]
FB["hinata.response_feedback"]
EMA["allmight.ema_readings"]
BURN["hinata.burnout_checks"]
end
MICHAEL((Michael)) -->|message| BOT
BOT -->|substantive reply<br/>+ inline keyboard| MICHAEL
MICHAEL -->|button press| BOT
BOT -->|INSERT feedback| FB
SCHED -->|3x daily<br/>conditional| MICHAEL
MICHAEL -->|EMA button press| BOT
BOT -->|INSERT reading| EMA
FB -->|weekly aggregate| OROQ["Orochimaru<br/>scout query"]
EMA -->|trend query| ALLQ["AllMight<br/>burnout heuristic"]
ALLQ -->|write| BURN
BURN -->|threshold alert| BOT
BOT -->|adjusted response<br/>style| MICHAEL1. Inline Keyboard Feedback
Data model
Table: hinata.response_feedback (CT100, hinata schema -- alongside existing burnout_checks, system_events)
sql
CREATE TABLE hinata.response_feedback (
id BIGSERIAL PRIMARY KEY,
message_id BIGINT NOT NULL, -- Telegram message_id of the bot response
chat_id BIGINT NOT NULL, -- Telegram chat_id (always Michael's)
rating TEXT NOT NULL, -- 'good' | 'bad' | 'wrong_commander' | 'too_long'
commander TEXT, -- commander active at response time (from RAG-lite)
followup_text TEXT, -- optional free-text Michael sends after rating
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_feedback_created ON hinata.response_feedback (created_at DESC);
CREATE INDEX idx_feedback_commander ON hinata.response_feedback (commander);Message flow
- After every concierge response (not capture-mode acks),
telegram-bot.pyattaches anInlineKeyboardMarkupto the reply. - Keyboard layout (single row, four buttons):
[ +1 ] [ -1 ] [ Wrong domain ] [ Too long ]Callback data encoding: fb:{message_id}:{rating} -- e.g. fb:12345:good.
CallbackQueryHandler receives the button press:
- Parse callback data.
- INSERT into
hinata.response_feedbackvia asyncpsycopg(connection pooled). - Answer the callback query with a brief ack ("Noted" / "Flagged -- will adjust").
- Edit the original message to remove the inline keyboard (prevents double-rating).
Optional followup capture: If Michael sends a text message within 30 seconds of a
-1orwrong_commanderrating, attach it asfollowup_textto the same feedback row (UPDATE by message_id).
Implementation in telegram-bot.py
python
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler
FEEDBACK_KEYBOARD = InlineKeyboardMarkup([
[
InlineKeyboardButton("+1", callback_data="fb:__MID__:good"),
InlineKeyboardButton("-1", callback_data="fb:__MID__:bad"),
InlineKeyboardButton("Wrong domain", callback_data="fb:__MID__:wrong_commander"),
InlineKeyboardButton("Too long", callback_data="fb:__MID__:too_long"),
]
])
def make_feedback_kb(message_id: int) -> InlineKeyboardMarkup:
"""Stamp the message_id into the callback_data template."""
return InlineKeyboardMarkup([
[
InlineKeyboardButton("+1", callback_data=f"fb:{message_id}:good"),
InlineKeyboardButton("-1", callback_data=f"fb:{message_id}:bad"),
InlineKeyboardButton("Wrong domain", callback_data=f"fb:{message_id}:wrong_commander"),
InlineKeyboardButton("Too long", callback_data=f"fb:{message_id}:too_long"),
]
])Attach to reply in handle_message:
python
sent = await update.message.reply_text(response_text, parse_mode="Markdown")
# Only for concierge (substantive) responses
if concierge_available():
await sent.edit_reply_markup(reply_markup=make_feedback_kb(sent.message_id))Orochimaru consumption
Orochimaru's weekly scout query (Sunday 21:00) reads aggregated feedback:
sql
-- Commander quality score for the week
SELECT commander,
COUNT(*) FILTER (WHERE rating = 'good') AS positive,
COUNT(*) FILTER (WHERE rating = 'bad') AS negative,
COUNT(*) FILTER (WHERE rating = 'wrong_commander') AS misrouted,
COUNT(*) FILTER (WHERE rating = 'too_long') AS verbose
FROM hinata.response_feedback
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY commander
ORDER BY negative DESC;This feeds the "response quality" dimension in Meruem's 5-dimension maturity scoring.
2. EMA Scoring (Ecological Momentary Assessment)
Data model
Table: allmight.ema_readings (CT100, new allmight schema -- AllMight-health owns this data)
sql
CREATE SCHEMA IF NOT EXISTS allmight;
CREATE TABLE allmight.ema_readings (
id BIGSERIAL PRIMARY KEY,
energy SMALLINT NOT NULL CHECK (energy BETWEEN 1 AND 5),
mood SMALLINT NOT NULL CHECK (mood BETWEEN 1 AND 5),
focus SMALLINT NOT NULL CHECK (focus BETWEEN 1 AND 5),
followup_text TEXT, -- optional free-text ("What's on your mind?")
prompt_type TEXT NOT NULL DEFAULT 'scheduled', -- 'scheduled' | 'manual' | 'burnout_followup'
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_ema_created ON allmight.ema_readings (created_at DESC);Prompt schedule and logic
Three daily windows, using APScheduler (already available in the python-telegram-bot ecosystem via JobQueue):
| Window | Target time | Variation | Condition |
|---|---|---|---|
| Morning | 09:00 | +/- 15 min random | Michael messaged bot in last 4 hours |
| Midday | 13:00 | +/- 15 min random | Michael messaged bot in last 4 hours |
| Evening | 20:00 | +/- 15 min random | Michael messaged bot in last 4 hours |
Attention boundary rule: Before sending an EMA prompt, check _last_user_message_ts (already tracked by the bot's session state). If now - _last_user_message_ts > 4 hours, skip this window silently. Do not queue -- the reading is simply missed. This respects the noise policy.
Random jitter: Add random.randint(-15, 15) minutes to each scheduled time at bot startup to prevent the check-in feeling robotic. Recompute jitter daily.
EMA inline keyboard flow
The EMA prompt is a three-stage inline keyboard conversation:
Stage 1 -- Energy:
How's your energy?
[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]Callback data: ema:energy:{value}
Stage 2 -- Mood (sent after energy selection):
Mood?
[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]Callback data: ema:mood:{value}
Stage 3 -- Focus (sent after mood selection):
Focus?
[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]Callback data: ema:focus:{value}
Stage 4 -- Optional followup (sent after focus selection):
Anything on your mind? (send a message or tap Skip)
[ Skip ]Callback data: ema:skip
If Michael sends a text message instead of tapping Skip, capture it as followup_text.
State tracking: Use context.user_data["ema_pending"] dict to hold partial readings while the 3-stage flow completes. Structure:
python
context.user_data["ema_pending"] = {
"energy": None, # filled after stage 1
"mood": None, # filled after stage 2
"focus": None, # filled after stage 3
"followup": None, # filled after stage 4 or skip
"started_at": datetime.now(timezone.utc),
}On completion (all three scores + skip/text), INSERT into allmight.ema_readings and clear ema_pending.
Timeout: If ema_pending is older than 30 minutes when the next message arrives, discard it silently. Stale partial readings are noise.
Manual trigger
/checkin command triggers an EMA prompt immediately, bypassing the schedule and attention boundary. Uses prompt_type = 'manual'.
JobQueue setup (in main)
python
from telegram.ext import JobQueue
async def ema_job(context: ContextTypes.DEFAULT_TYPE):
"""Scheduled EMA prompt -- respects attention boundary."""
last_msg = context.bot_data.get("last_user_message_ts")
if last_msg is None:
return # no interaction this session
gap = (datetime.now(timezone.utc) - last_msg).total_seconds()
if gap > 4 * 3600:
log.info("EMA skipped -- no user message in last 4h")
return
await send_ema_prompt(context, int(CHAT_ID))
# In main(), after app.build():
job_queue = app.job_queue
job_queue.run_daily(ema_job, time=time(hour=9, minute=0), name="ema_morning")
job_queue.run_daily(ema_job, time=time(hour=13, minute=0), name="ema_midday")
job_queue.run_daily(ema_job, time=time(hour=20, minute=0), name="ema_evening")3. Burnout Risk Integration
Signal sources
| Signal | Source | Weight |
|---|---|---|
| EMA trend | allmight.ema_readings -- 3+ consecutive readings with avg < 2.5 across dimensions | Primary |
| Token burn spike | orochimaru.token_burn_daily -- daily burn > 2x 7-day rolling average | Secondary |
| Combined signal | High token burn + low energy EMA in same day | Critical |
Detection logic
Run after every EMA INSERT (trigger in the callback handler, not a separate cron):
python
async def check_burnout_risk(pool) -> str | None:
"""Returns 'RED' | 'AMBER' | None based on recent EMA readings + token burn."""
async with pool.acquire() as conn:
# Last 3 EMA readings
rows = await conn.fetch("""
SELECT energy, mood, focus, created_at
FROM allmight.ema_readings
ORDER BY created_at DESC LIMIT 3
""")
if len(rows) < 3:
return None
avg_energy = sum(r['energy'] for r in rows) / 3
avg_mood = sum(r['mood'] for r in rows) / 3
avg_focus = sum(r['focus'] for r in rows) / 3
composite = (avg_energy + avg_mood + avg_focus) / 3
# Check token burn spike (today vs 7-day avg)
burn_row = await conn.fetchrow("""
SELECT
COALESCE((SELECT total_tokens FROM orochimaru.token_burn_daily
WHERE burn_date = CURRENT_DATE), 0) AS today,
COALESCE((SELECT AVG(total_tokens) FROM orochimaru.token_burn_daily
WHERE burn_date > CURRENT_DATE - 7), 1) AS avg_7d
""")
burn_spike = (burn_row['today'] / burn_row['avg_7d']) > 2.0 if burn_row['avg_7d'] > 0 else False
# Decision
if composite < 2.0 and burn_spike:
return 'RED'
elif composite < 2.5:
return 'AMBER'
elif composite < 3.0 and burn_spike:
return 'AMBER'
return NoneResponse adjustment
When burnout risk is detected, the bot modifies its behaviour for the remainder of the session:
| Risk level | Bot adjustments |
|---|---|
| AMBER | Append to system prompt: "Michael may be fatigued. Keep responses under 3 sentences. End with one supportive note." |
| RED | Same as AMBER + proactive message: "You've been running hard. Consider stepping away for a bit." + suppress non-critical notifications for 2 hours |
Storage: Write risk level to hinata.burnout_checks (existing table) with source 'ema_trigger':
sql
INSERT INTO hinata.burnout_checks (risk_level, rationale, source, created_at)
VALUES ($1, $2, 'ema_trigger', NOW());Session flag: Set context.bot_data["burnout_mode"] = "AMBER" (or "RED"). The concierge response builder checks this flag and adjusts the system prompt before calling the Anthropic API.
AllMight weekly flag integration
AllMight's existing burnout-risk-weekly-flag ambient reads from allmight.ema_readings instead of (currently unwired) session-log heuristics. This wires the "energy signal" input that AllMight's burnout flag has been waiting for (see data-plumbing gap noted in AllMight context).
4. Database Connection
CT106 connects to CT100 postgres over the LAN (192.168.1.253:5432). Connection pool initialised at bot startup:
python
import asyncpg
DB_DSN = "postgresql://hinata_bot:$PASSWORD@192.168.1.253:5432/hinata"
async def init_db_pool():
return await asyncpg.create_pool(DB_DSN, min_size=1, max_size=3)Credential: Store the postgres password in CT106 env (/etc/hinata/telegram.env) as POSTGRES_DSN. Load alongside existing credential flow. Register in Vaultwarden via Itachi.
Fallback: If postgres is unreachable, log feedback/EMA to local JSON files (/tmp/hinata-feedback-buffer.jsonl, /tmp/hinata-ema-buffer.jsonl). A retry loop flushes the buffer every 5 minutes. This prevents feedback loss during CT100 maintenance windows.
5. Implementation Phases
Phase 1 -- Feedback keyboard (1 session)
- [ ] Create
hinata.response_feedbacktable on CT100 - [ ] Add
make_feedback_kb()to telegram-bot.py - [ ] Add
CallbackQueryHandlerforfb:*pattern - [ ] Attach keyboard to concierge responses in
handle_message - [ ] Wire asyncpg connection pool + INSERT
- [ ] Add local JSON fallback buffer
- [ ] Test: send message, rate it, verify row in postgres
Phase 2 -- EMA prompts (1 session)
- [ ] Create
allmightschema andema_readingstable on CT100 - [ ] Implement
send_ema_prompt()3-stage keyboard flow - [ ] Add
CallbackQueryHandlerforema:*pattern - [ ] Track partial readings in
context.user_data["ema_pending"] - [ ] Add
/checkincommand - [ ] Wire
JobQueuefor 3x daily schedule - [ ] Add attention boundary check (4-hour rule)
- [ ] Test: trigger
/checkin, complete all 3 stages, verify row in postgres
Phase 3 -- Burnout integration (1 session)
- [ ] Implement
check_burnout_risk()triggered after every EMA INSERT - [ ] Write risk flags to
hinata.burnout_checks - [ ] Add
burnout_modesession flag and system prompt injection - [ ] Wire proactive message for RED risk
- [ ] Add notification suppression for RED (2-hour window)
- [ ] Test: insert low EMA scores, verify AMBER/RED detection and prompt adjustment
Phase 4 -- Orochimaru consumption (next scout cycle)
- [ ] Add feedback aggregation query to Orochimaru weekly scout
- [ ] Add EMA trend summary to AllMight burnout-risk-weekly-flag
- [ ] Surface misrouted-commander count in Meruem evolution assessment
6. Dependencies and Blockers
| Dependency | Status | Blocker? |
|---|---|---|
asyncpg installed in CT106 venv | Not yet | Yes -- pip install asyncpg in CT106 |
| CT106 network access to CT100:5432 | Existing (LAN) | No |
hinata schema on CT100 | Exists (renamed 2026-06-08) | No |
allmight schema on CT100 | Does not exist | Yes -- CREATE SCHEMA |
orochimaru.token_burn_daily table | Exists | No |
| Postgres bot user + password | Not yet | Yes -- create role, register in Vaultwarden |
python-telegram-bot InlineKeyboard support | Exists (v20+) | No |
python-telegram-bot JobQueue | Exists (v20+) | No |
| AllMight burnout flag data plumbing | Unwired (see context) | No -- this spec wires it |
Security notes
- Postgres DSN loaded from env, never hardcoded.
- Bot user gets INSERT/SELECT only on
hinata.response_feedback,allmight.ema_readings,hinata.burnout_checks, and SELECT onorochimaru.token_burn_daily. No DELETE, no DDL. - Callback data is validated server-side (regex match on
fb:{int}:{enum}andema:{dimension}:{int}). Malformed callbacks are silently dropped. - EMA readings contain no PII beyond the implicit "this is Michael's data" -- no user ID stored, single-tenant by design.
Cross-links: federation/captain_canary-organiser_context | federation/colonel_saitama-foundation_allmight-health_context | federation/captain_orochimaru-scout_context | supreme-court/telegram/telegram-noise-policy | the-government/information_reference/reference_z2-service-catalog