06 · Proactive Decision-Making & Gifts
Sixth installment of the architecture deep-dive: the Decider local small model handles "should I proactively act?", the APScheduler scheduling system, report/reminder/schedule/todo pipelines, and the gift system. Corresponds to
ARCHITECTURE.md§7.
§7.1 Why Train a Local Small Model (Decider)
| Requirement | Cloud LLM | Local Decider |
|---|---|---|
| Latency | 200-2000ms | <50ms |
| Privacy | Data leaves the machine | Fully local |
| Frequency | One API call per signal | Unlimited frequency |
| Cost | Token-based billing | Zero marginal cost |
Proactive decision-making (whether to remind, whether to report, whether to proactively fetch information) must be low-latency, local, and private. Relational prompt-based judgments are too expensive to run through a cloud LLM → train a small classification model to act as a gating mechanism.
§7.2 Proactive Decision Architecture
Event/Signal → Assistant.evaluate_signal()
→ DeciderService.shared().evaluate(agent_id, trigger_type, ...)
→ DecisionOutcome(fire: bool, action_category: str, act_score: float)
├─fire=False → Ignore
└─fire=True → Assistant.send_proactive_message(...)
→ dialogue.tree.attach_proactive_node(content, metadata)
→ reminder_dispatch._emit(event) → inbox.jsonl → SSE
→ Also send native OS notification7.2.1 evaluate_signal Parameters
def evaluate_signal(
self,
*,
trigger_type: str, # "new_message" / "external_info" / ...
trigger_summary: str, # Trigger summary (≤300 characters)
trigger_details: Optional[dict] = None,
memory: Optional[list[str]] = None,
user_state: Optional[dict] = None,
recent_user_messages: Optional[list[str]] = None,
):7.2.2 send_proactive_message
def send_proactive_message(
self,
*,
content: str,
signal_summary: str,
action_category: str,
act_score: Optional[float] = None,
decision_id: Optional[str] = None,
dialogue_id: int = 0,
title: Optional[str] = None,
fired_at: Optional[float] = None,
extra: Optional[dict] = None, # e.g. {"gift": {...}} triggers rich rendering on the frontend
) -> Optional[int]:
"""Returns the new node id, or None (if there is no dialogue).
metadata contains decision_id / signal_summary / action_category / act_score →
frontend scoring UI ("right/wrong") POSTs to /api/feedback/proactive.
"""7.2.3 DeciderService Internal Logic
# src/decider/service.py
class DeciderService:
"""Threshold / cooldown / crisis-keyword / DND logic.
evaluate() returns a DecisionOutcome and never raises an exception.
Worst case: fire=False + source="predict_error".
"""
@classmethod
def shared(cls) -> "DeciderService":
"""Process-level singleton."""§7.3 APScheduler Scheduling System
7.3.1 Scheduler Types
# src/utils/daily_tools.py
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger| Schedule Type | Purpose | Trigger |
|---|---|---|
cron | Scheduled reports/cleanup | CronTrigger |
schedule | Schedule reminders | One-shot / recurring |
todo | Todo reminders | Before deadline |
reminder | General reminders | 60s interval scan |
7.3.2 Recurring Schedules (v13 recurrence)
_FREQ_VALID = {"never", "daily", "weekly", "monthly", "yearly"}
def start_of_cycle(dt: datetime, frequency: str) -> datetime:
"""Returns the start of the cycle that dt belongs to.
daily → Midnight today
weekly → Midnight of this ISO Monday
monthly → Midnight of the 1st of this month
yearly → Midnight of January 1st this year
never → dt truncated to midnight
"""ScheduleList.create_schedule seeds last_cycle_start, and DailyTools._run_recurrence_rollover enumerates occurrences.
7.3.3 Schedule State Machine
pending → active → completed
→ cancelled
→ expired (deadline passed)v8__schedules_status.sql adds the status column.
§7.4 Reminder Dispatch (src/utils/reminder_dispatch.py)
7.4.1 Architecture
def _inbox_path(workspace_route: str) -> Path:
return Path(workspace_route) / "workspace" / "reminders" / "inbox.jsonl"
def _emit(workspace_route: str, event: dict) -> None:
"""Append a reminder event to the inbox JSONL. The bridge tails this file."""
event = {**event, "ts": time.time()}
inbox = _inbox_path(workspace_route)
with inbox.open("a", encoding="utf-8") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
_maybe_native(event)7.4.2 Reminders ≠ Chat Messages
| Dimension | Reminder | Chat Message |
|---|---|---|
| Dialogue tree | Not inserted (no Q/A node) | Inserted |
| LLM | Not consumed | Consumed |
| History | Not added to ConversationSession.history | Added |
| Frontend rendering | ReminderToast overlay | Chat bubble |
| SSE stream | /api/reminders/stream | /api/chat/stream |
7.4.3 Native Notifications
def _maybe_native(event: dict) -> None:
"""Send a native OS notification for due reminders. Always fires.
Reminders must reach the user regardless of whether the app window is visible.
The inbox always carries the payload; the native window channel fires in a
worker thread, so a slow PowerShell/osascript never blocks the DB scan.
Double-trigger protection: skip the daemon toast when the renderer is
already attached → prevents two identical OS notifications.
"""7.4.4 Notification Deduplication
Uses client_presence's _active counter to determine whether the renderer is online:
- Renderer online → renderer path handles it → daemon skips native toast
- Renderer offline → daemon sends native toast
§7.5 DailyTools
# src/utils/daily_tools.py
class DailyTools:
"""Collection of schedule / todo / reminder / report tools.
Integrates with APScheduler, providing:
- create_schedule / update_schedule / list_schedules
- create_todo / update_todo / list_todos
- create_reminder / list_reminders
- _run_recurrence_rollover() # 60s interval scan for recurring schedules
"""7.5.1 Report Generation (Deprecated)
# src/models/assistant.py
def write_report(self, type: str = "day", timestamp=None) -> str:
"""Deprecated: Per-agent daily/weekly/monthly/annual reports
have been replaced by the manager_review skill.
This stub is kept so that any residual import fails loudly
rather than silently.
"""
raise RuntimeError(
"Assistant.write_report is deprecated — use the manager_review "
"skill (triggered per-agent by dialogue-turn threshold)."
)7.5.2 manager_review Skill
workspace/manager/skills/manager_review/SKILL.mdTriggered by per-agent dialogue-turn thresholds, replacing the old cron-based reports.
§7.6 Gift Pipeline
7.6.1 Architecture
User sends a gift → /api/gift/... → gift.py route handles it
→ Redemption code verification
→ Token/coin transfer
→ Guild settlement7.6.2 Gift-Triggered Proactive Messages
# The extra parameter of Assistant.send_proactive_message
extra = {"gift": {...}} # The frontend keys off this field to switch to a rich-rendering bubbleThe extra dict from send_proactive_message is merged into metadata; the frontend renderer checks for the presence of metadata.gift → switches to a custom chat bubble.
§7.7 Decider VRAM Management
The Decider model's position in the VRAM gate:
| Condition | Priority | Category |
|---|---|---|
| mode0 (no GPU) | N/A | Uses cloud LLM 2-call hack |
| tier4 (~4GB) | 3 — NICE | queue, triggered when GPU is idle |
| tier6 (~6GB) | 3 — NICE | queue, bf16 |
| tier8 (≥8GB) | 2 — IMPORTANT | sync, always resident |
Next: 07-Digital Human →
