feat(todo-continuation): integrate todo continuation into TUI

Phase 0.2 of Always-On Personas architecture:

- Add toast notification on session switch when incomplete todos exist
  (routes/session/index.tsx)
- Inject system reminder into LLM context for incomplete tasks
  (session/prompt.ts)
- Add visual hint in prompt area showing current todo progress
  (component/prompt/index.tsx)
- Create architecture roadmap for Always-On Personas system
  (docs/architecture/ALWAYS-ON-PERSONAS.md)

This enables automatic continuation of incomplete tasks when:
1. Switching to a session with pending todos (toast + hint)
2. Sending any prompt in a session with incomplete tasks (system reminder)

Part of the headless agent infrastructure for remote access via Zee.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-09 18:45:54 +01:00
parent 4c893c4624
commit bb5857fb3b
4 changed files with 495 additions and 1 deletions
+401
View File
@@ -0,0 +1,401 @@
# Always-On Personas: Headless Agent Architecture
## Vision
Turn on your PC in the morning, leave without logging in, and communicate with the Triad (Zee/Stanley/Johny) via your phone. The personas work autonomously on your home computer, accessible 24/7.
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ALWAYS-ON ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────────────────────────────────┐ │
│ │ PHONE │ │ HOME PC (Headless) │ │
│ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────────────────────────────────┐ │ │
│ │ │Telegram │◄├─────┼──┤ agent-core daemon │ │ │
│ │ │WhatsApp │ │ │ │ (systemd service, pre-login) │ │ │
│ │ │ Discord │ │ │ └──────────────┬──────────────────────┘ │ │
│ │ └─────────┘ │ │ │ │ │
│ │ │ │ │ ┌──────────────┼──────────────────────┐ │ │
│ │ ▼ │ │ │ ▼ │ │ │
│ │ ┌─────────┐ │ │ │ ┌─────┐ ┌─────────┐ ┌───────┐ │ │ │
│ │ │ ZEE │◄├─────┼──┼──┤ ZEE │ │ STANLEY │ │ JOHNY │ │ │ │
│ │ │ Gateway │ │ │ │ └──┬──┘ └────┬────┘ └───┬───┘ │ │ │
│ │ └─────────┘ │ │ │ │ │ │ │ │ │
│ └─────────────┘ │ │ └──────────┼──────────┘ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ SHARED LAYER │ │ │ │
│ │ │ │ • Qdrant Memory │ │ │ │
│ │ │ │ • Session Persistence │ │ │ │
│ │ │ │ • Todo Continuation │ │ │ │
│ │ │ │ • Hook System │ │ │ │
│ │ │ └─────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ TIARA ORCHESTRATION │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ WezTerm (when logged in) │ │ │
│ │ │ • Visual panes for each drone │ │ │
│ │ │ • Real-time status │ │ │
│ │ │ • Optional - system works without │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Implementation Phases
### Phase 0: Todo Continuation Integration (Current)
**Status: In Progress**
Immediate integration of todo-continuation into existing systems.
#### 0.1 CLI Hook System (DONE)
- [x] `todo-continuation` hook type in tiara
- [x] `session-restore` triggers todo-continuation
- [x] `session-end` saves todos to memory
- [x] CLI command: `claude hook todo-continuation`
#### 0.2 TUI Integration (TODO)
- [ ] **Toast notification** - When switching to session with incomplete todos
- [ ] **Backend system reminder** - Inject reminder into conversation context
- [ ] **Prompt hint** - Visual indicator in prompt area about pending tasks
#### 0.3 Session Persistence Hardening
- [ ] Ensure todos survive TUI restart
- [ ] Validate session state on load
- [ ] Add session recovery on crash
---
### Phase 1: Headless Daemon Mode
**Prerequisites: Phase 0 complete**
agent-core runs as a system service, starting before user login.
#### 1.1 Systemd Service
```bash
# /etc/systemd/system/agent-core.service
[Unit]
Description=Agent Core - Always-On Personas
After=network.target
[Service]
Type=simple
User=artur
Environment=HOME=/home/artur
Environment=AGENT_CORE_HEADLESS=1
WorkingDirectory=/home/artur/Repositories/agent-core
ExecStart=/home/artur/.bun/bin/bun run packages/opencode/src/cli/index.ts daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
#### 1.2 Daemon Mode Implementation
- [ ] Create `daemon` command in CLI
- [ ] Headless session management (no TUI)
- [ ] API server for remote communication
- [ ] Credential management without user session
- [ ] Logging to file/journald
#### 1.3 Credential Access
- [ ] Keyring access from service context
- [ ] Environment-based fallback for API keys
- [ ] Secure credential storage at `~/.zee/credentials/`
---
### Phase 2: Remote Communication Gateway
**Prerequisites: Phase 1 complete**
Zee becomes the universal gateway for all communication.
#### 2.1 Messaging Platform Integration
```
┌──────────────────────────────────────────────────────────────┐
│ ZEE GATEWAY │
├──────────────────────────────────────────────────────────────┤
│ │
│ Telegram ──┐ │
│ │ ┌──────────────────────────────────────┐ │
│ WhatsApp ──┼───►│ Message Router │ │
│ │ │ │ │
│ Discord ───┘ │ • Parse intent │ │
│ │ • Route to persona (Zee/Stanley/ │ │
│ │ Johny) │ │
│ │ • Queue if busy │ │
│ │ • Return results │ │
│ └──────────────────────────────────────┘ │
│ │
│ Outbound: │
│ • Task completion notifications │
│ • Todo status updates │
│ • Error alerts │
│ • Daily summaries │
└──────────────────────────────────────────────────────────────┘
```
#### 2.2 Implementation Tasks
- [ ] Telegram bot running in daemon mode
- [ ] WhatsApp integration (if available)
- [ ] Discord bot as alternative
- [ ] Message queue for handling during offline
- [ ] Intent parsing to route to correct persona:
- "Check my portfolio" → Stanley
- "What's on my calendar" → Zee
- "Quiz me on calculus" → Johny
#### 2.3 Security Considerations
- [ ] Device authentication
- [ ] Rate limiting
- [ ] Audit logging
- [ ] Session tokens for multi-device
---
### Phase 3: Session Persistence & Recovery
**Prerequisites: Phase 1, 2 in progress**
Robust session management that survives anything.
#### 3.1 Persistent Session Store
```
~/.zee/
├── sessions/
│ ├── active/ # Currently active sessions
│ │ ├── {session-id}.json
│ │ └── {session-id}.todos.json
│ └── archive/ # Completed sessions
├── state/
│ ├── daemon.pid # Daemon process ID
│ ├── daemon.lock # Lock file
│ └── last-active.json # Last active session per persona
└── recovery/
├── checkpoint/ # Periodic state snapshots
└── journal/ # Write-ahead log for crash recovery
```
#### 3.2 Recovery Mechanisms
- [ ] Checkpoint every N minutes
- [ ] Write-ahead logging for in-progress operations
- [ ] Graceful shutdown with state save
- [ ] Crash recovery on restart:
1. Load last checkpoint
2. Replay journal entries
3. Trigger todo-continuation hooks
4. Resume interrupted work
#### 3.3 Cross-Device Session Continuity
- [ ] Phone starts a conversation
- [ ] Continue on desktop
- [ ] Session context follows the user
---
### Phase 4: Tiara Hook Integration
**Prerequisites: Phase 0.1 complete, Phase 1-3 in progress**
Full hook lifecycle for session management.
#### 4.1 Hook Events
```typescript
// Session lifecycle
'session-start' // New session created
'session-restore' // Existing session loaded (triggers todo-continuation)
'session-end' // Session completed or suspended
'session-transfer' // Session moving to different device/context
// Todo lifecycle
'todo-continuation' // Incomplete tasks detected
'todo-completed' // All tasks done
'todo-blocked' // Task cannot proceed, needs input
// Daemon lifecycle
'daemon-start' // Daemon starting up
'daemon-ready' // Daemon ready to accept work
'daemon-shutdown' // Graceful shutdown initiated
```
#### 4.2 Integration Points
- [ ] Daemon startup triggers `session-restore` for all active sessions
- [ ] Message gateway triggers `session-start` or `session-restore`
- [ ] TUI session switch triggers `session-restore`
- [ ] All paths check for todo-continuation
---
### Phase 5: Visual Orchestration (Optional Enhancement)
**Prerequisites: All previous phases**
When you're at your desk, see what the personas are doing.
#### 5.1 WezTerm Integration
- [ ] Daemon spawns WezTerm panes when X session available
- [ ] One pane per active drone/task
- [ ] Status pane showing overall system state
- [ ] Graceful degradation when no display
#### 5.2 Web Dashboard (Alternative)
- [ ] Local web server in daemon
- [ ] Real-time status via WebSocket
- [ ] Access from any device on local network
- [ ] Mobile-friendly interface
---
## Current Status
### Completed
- Todo continuation hook infrastructure in tiara
- CLI integration for session-restore with todo-continuation
- Session storage in opencode sync context
### In Progress
- TUI integration (toast, backend reminder, prompt hint)
### Next Steps
1. Implement TUI todo-continuation integrations
2. Create daemon mode skeleton
3. Design message gateway architecture
4. Implement persistent session store
---
## Technical Decisions
### Why Zee as Gateway?
Zee is already the personal assistant with messaging capabilities. Rather than create a separate "remote access" system, leverage Zee's existing role:
- Natural language interface
- Already handles calendar, contacts, notifications
- Can delegate to Stanley/Johny as needed
- Single point of entry reduces complexity
### Why Systemd?
- Starts before user login
- Automatic restart on failure
- Journal integration for logs
- Well-understood, reliable
### Why Not Docker/Container?
- Needs access to user's files and credentials
- GPU access for potential local LLM
- Desktop integration (WezTerm, notifications)
- Containers add complexity without benefit here
### Session Storage: SQLite vs JSON Files
For now: JSON files
- Simple, debuggable
- Easy to backup/restore
- No database dependencies
Later: Consider SQLite if:
- Query patterns become complex
- Concurrent access is needed
- Performance becomes an issue
---
## Configuration
```yaml
# ~/.config/agent-core/daemon.yaml
daemon:
enabled: true
autostart: true
# Session management
session:
persistence: true
checkpoint_interval: 300 # seconds
recovery: true
# Todo continuation
todo:
auto_continue: true
notify_on_incomplete: true
completion_threshold: 100 # percent before considering done
# Remote access
gateway:
telegram:
enabled: true
bot_token: ${TELEGRAM_BOT_TOKEN}
whatsapp:
enabled: false # future
discord:
enabled: false # future
# Visual (when available)
wezterm:
enabled: true
auto_spawn_panes: true
```
---
## Success Criteria
### Phase 0 Complete When:
- [ ] Switching sessions in TUI shows toast for incomplete todos
- [ ] LLM receives system reminder about pending tasks
- [ ] Prompt area shows hint about incomplete work
### Phase 1 Complete When:
- [ ] `systemctl start agent-core` works
- [ ] Survives reboot
- [ ] Logs accessible via `journalctl -u agent-core`
### Phase 2 Complete When:
- [ ] Send Telegram message → get response from Zee
- [ ] "Ask Stanley about NVDA" → Stanley analyzes
- [ ] Notifications sent for completed tasks
### Phase 3 Complete When:
- [ ] Kill daemon → restart → work continues
- [ ] Reboot PC → work continues
- [ ] Start conversation on phone → continue on desktop
### Phase 4 Complete When:
- [ ] All lifecycle hooks fire correctly
- [ ] Consistent behavior across CLI, TUI, daemon, remote
### Phase 5 Complete When:
- [ ] Login to desktop → see active work in WezTerm
- [ ] Web dashboard shows system status
- [ ] Can monitor from phone browser
---
## Appendix: Message Routing Examples
```
User (via Telegram): "What's my portfolio looking like?"
→ Zee: Detects finance intent
→ Route to Stanley
→ Stanley: Analyzes portfolio
→ Zee: Formats and sends response
User (via Telegram): "Remind me about the API meeting tomorrow"
→ Zee: Handles directly (calendar + notification)
→ Response sent
User (via Telegram): "Quiz me on derivatives"
→ Zee: Detects study intent
→ Route to Johny
→ Johny: Generates quiz based on knowledge graph
→ Zee: Formats and sends quiz
User (via Telegram): "Continue working on the auth feature"
→ Zee: Detects code intent with active todo
→ Restore session with todo-continuation
→ Resume work autonomously
→ Zee: Sends completion notification when done
```
@@ -87,6 +87,27 @@ export function Prompt(props: PromptProps) {
const textareaKeybindings = useTextareaKeybindings()
// Track incomplete todos for hint display
const incompleteTodos = createMemo(() => {
if (!props.sessionID) return []
const todos = sync.data.todo[props.sessionID] ?? []
return todos.filter((t) => t.status !== "completed")
})
const todoHint = createMemo(() => {
const incomplete = incompleteTodos()
if (incomplete.length === 0) return null
const todos = sync.data.todo[props.sessionID ?? ""] ?? []
const completed = todos.filter((t) => t.status === "completed").length
const inProgress = incomplete.find((t) => t.status === "in_progress")
return {
count: incomplete.length,
completed,
total: todos.length,
current: inProgress?.content?.slice(0, 30) ?? incomplete[0]?.content?.slice(0, 30),
}
})
const fileStyleId = syntax().getStyleId("extmark.file")!
const agentStyleId = syntax().getStyleId("extmark.agent")!
const pasteStyleId = syntax().getStyleId("extmark.paste")!
@@ -985,7 +1006,18 @@ export function Prompt(props: PromptProps) {
/>
</box>
<box flexDirection="row" justifyContent="space-between">
<Show when={status().type !== "idle"} fallback={<text />}>
<Show
when={status().type !== "idle"}
fallback={
<Show when={todoHint()}>
{(hint) => (
<text fg={theme.warning}>
[{hint().completed}/{hint().total}] {hint().current}...
</text>
)}
</Show>
}
>
<box
flexDirection="row"
gap={1}
@@ -232,6 +232,23 @@ export function Session() {
.sync(route.sessionID)
.then(() => {
if (scroll) scroll.scrollBy(100_000)
// Check for incomplete todos and show continuation reminder
const todos = sync.data.todo[route.sessionID] ?? []
const incompleteTodos = todos.filter((t) => t.status !== "completed")
const completedCount = todos.length - incompleteTodos.length
if (incompleteTodos.length > 0 && todos.length > 0) {
const inProgress = todos.filter((t) => t.status === "in_progress")
const pending = todos.filter((t) => t.status === "pending")
toast.show({
variant: "info",
title: "Todo Continuation",
message: `${completedCount}/${todos.length} completed. ${inProgress.length > 0 ? `Working on: ${inProgress[0].content.slice(0, 40)}...` : `${pending.length} pending tasks.`}`,
duration: 5000,
})
}
})
.catch((e) => {
console.error(e)
+44
View File
@@ -34,6 +34,7 @@ import { Command } from "../command"
import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { Todo } from "./todo"
import { NamedError } from "@opencode-ai/util/error"
import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
@@ -1335,6 +1336,49 @@ NOTE: At any point in time through this workflow you should feel free to ask the
userMessage.parts.push(part)
return input.messages
}
// Todo continuation reminder
const todos = await Todo.get(input.session.id)
if (todos.length > 0) {
const incompleteTodos = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled")
const inProgress = todos.filter((t) => t.status === "in_progress")
const pending = todos.filter((t) => t.status === "pending")
const completedCount = todos.filter((t) => t.status === "completed").length
if (incompleteTodos.length > 0) {
const todoList = incompleteTodos
.slice(0, 5)
.map((t) => `- [${t.status === "in_progress" ? "IN PROGRESS" : "PENDING"}] ${t.content}`)
.join("\n")
const reminderText = [
"<system-reminder>",
"[TODO CONTINUATION]",
"",
`You have ${incompleteTodos.length} incomplete tasks (${completedCount}/${todos.length} completed):`,
todoList,
incompleteTodos.length > 5 ? `... and ${incompleteTodos.length - 5} more` : "",
"",
"Continue working on these tasks:",
"- Proceed without asking for permission",
"- Mark each task complete when finished",
"- Do not stop until all tasks are done",
"</system-reminder>",
]
.filter(Boolean)
.join("\n")
userMessage.parts.push({
id: Identifier.ascending("part"),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: reminderText,
synthetic: true,
})
}
}
return input.messages
}