Appearance
Goals, todos, and follow-through
One visa-planning thread produced three different results:
- "Prepare the application" remained a useful long-term outcome.
- "Collect bank statements" was unfinished work in that thread.
- "Remind me Friday to book the appointment" never fired.
The first two survived only because a person could recover them from the transcript. The third failed because no process reads conversational promises on a schedule. Desired outcomes, thread work, and future triggers need separate records. A goal says where the user wants to go. A todo records unfinished work in one thread. An intent gives the runtime permission and enough information to wake later.
Prompting the model to "follow through" cannot replace those records.
Compact concept map
text
goal
long-lived desired outcome
-> informs suggestions and planning
todo
unfinished work inside one thread
-> visible checklist
-> completed by user or agent
intent
future wake-up condition plus instruction
-> at | cron | event | heartbeat
-> isolated agent turn
-> message, silent result, retry, or failureThe three records can refer to the same project without sharing a lifecycle. "Learn conversational Japanese" may remain active for months. "Compare two tutors" may close today. "Remind me Friday to book a trial lesson" should fire once.
Goals describe direction
Goals belong in a synced table with a title, optional detail, status, sort order, and timestamps. They should not be inferred forever from one remark. Automatic capture may propose a learning goal, but the user needs a direct way to update, pause, complete, or remove it.
Active goals can enter the system context because they change useful behavior. A daily suggestion poller might propose one article related to an active goal. Completed goals should leave that path. This is why a status column is better than a line in a memory document.
Keep the count small. Ten active goals are easier to inspect and more useful to a model than an unbounded wish list.
Todos keep thread work honest
Todos answer a narrower question: what did this conversation start but not finish? A thread can store one revisioned checklist and expose four operations:
- list open items
- add an item
- complete an item
- reopen an item
The current open items can appear in the next prompt, capped at a modest number such as ten. The assistant should add a todo when a requested task cannot finish in the current turn. It should complete the item when the work is actually done, not when it starts.
A structured checklist is easier to migrate, render, and validate than freeform Markdown. Existing Markdown checkboxes can be parsed on read, then serialized into the new form.
Intents make time and events executable
An intent needs more than a date. It needs a conversation destination, a title, an instruction, a kind, state, retry counters, next run time, cooldown, expiry, and an idempotency key.
ts
type Intent = {
id: string;
conversationId: string;
title: string;
instruction: string;
kind: "at" | "cron" | "event" | "heartbeat";
status: "active" | "paused" | "firing" | "done" | "failed";
nextRunAt?: number;
eventSource?: string;
eventFilter?: Record<string, unknown>;
cooldownMs: number;
maxFires?: number;
expiresAt?: number;
attempts: number;
operationKey: string;
};The scheduler claims due rows with database locking, marks them as firing, and runs an isolated agent turn. The intent instruction becomes the final user message. The turn can read relevant conversation context, but browser-dependent tools should be absent because no tab is waiting to execute them.
The result posts back into the original thread with provenance that names the intent. One-off reminders become done. Recurring schedules compute the next time. Event intents wait for another matching event after their cooldown.
Reliability is part of the feature
At-least-once processing means duplicate prevention must sit below the model. A scheduling tool should use the tool operation id as a unique key. If an outbox retry repeats the call, the database returns the existing intent.
The scheduler should persist each attempt in an intent_runs record. On process startup, it can recover rows left in firing. Failures may retry a bounded number of times, then become visible as failed. A hidden retry loop that never records its state is not follow-through.
Signed event hooks need the same discipline. Verify the HMAC before storing the event. Match only active intents for that source. Treat the payload as untrusted data. Apply cooldown and expiry before firing.
Heartbeat is a special recurring intent. An empty checklist should cost zero model calls. When the model reports that nothing needs attention, the run is recorded as silent and no chat message or push is sent.
Terminology
- Goal is a long-lived outcome the user wants.
- Todo is an actionable item scoped to a thread.
- Intent is a persisted future wake-up instruction.
- Trigger is the time or event condition that makes an intent due.
- Isolated turn is an agent run started without a new user message or browser tab.
- Idempotency key makes a repeated creation request return the same logical record.
- Provenance tells the UI why an assistant message appeared.
These names let the product explain its behavior. A "Came back about" label is more useful than an unexplained message that appears hours later.
Decisions and alternatives declined
Reuse the ordinary agent loop for scheduled work. A separate automation engine would duplicate prompts, tools, error handling, and message insertion. An isolated mode with a smaller tool set preserves one execution path.
Postgres remains authoritative. In-memory timers disappear on restart and cannot coordinate multiple workers. Database rows support claiming, recovery, history, and UI controls.
Approvals and retries become new turns and records. Suspending one model call for hours creates state that is harder to recover and inspect.
Transcript mining on every scheduler tick is expensive, ambiguous, and prone to inventing obligations from conversational language. One universal task table also hides important differences. Goals, todos, and intents have different completion rules, even when the UI groups them in one project.
Failure modes
- The model says it will remind the user but never creates an intent.
- An outbox retry creates two reminders.
- A crash leaves an intent stuck in firing.
- A recurring schedule ignores timezone or daylight-saving behavior.
- An event payload is treated as trusted instruction text.
- A completed todo remains in prompt context.
- A private thread can create a follow-up.
- A heartbeat with no checklist still calls the model.
- A failed run disappears without a visible record.
- A notification arrives without a link to the originating thread.
Field checklist
Memory supplies context, but follow-through creates new data and new delivery paths. The privacy contract must therefore change execution itself, not merely decorate the thread. That is the subject of Private means private. The memory distinctions behind these records are in Memory that earns its place.