Introduce a Spotify-Wrapped-style recap feature: five themed cards (Cover, Path, Vibe, Workflow, Closing) rendered per archetype palette, with keyboard/swipe navigation and image export via capture IPC. Add RecapList, RecapDetail, RecapExport views and recap component library. Wire recap:list/read/updated IPC channels through preload, document the retrieval-to-card contract in references/recap-patterns.md, and bundle dist-renderer for production use.
14 KiB
Obelisk Recap Patterns
Use this when Obelisk Intent Routing sends the recap intent. In practice,
that means the first word after /obelisk is recap; everything after it is
the recap target.
Do not self-trigger this reference from broad weekly/monthly summaries, charts,
rankings, shareable-card language, or playlist-style metaphors. Those are target
details only after the explicit recap intent has already selected this file.
This is an optional app handoff pattern. Obelisk stays agent-first: the agent queries sessions and memories, judges what matters, then fills card content. The app owns rendering, layout, export, and animation.
Common Period Targets
Treat the target after recap as normal user language. Common app-generated
targets:
/obelisk recap this week-- the current calendar week in the user's timezone./obelisk recap last week-- the previous calendar week./obelisk recap this month-- the current calendar month./obelisk recap last month-- the previous calendar month.
Use the current date and timezone from the runtime/session context when available. If the target contains extra style language, keep the period meaning and treat the rest as presentation guidance.
Contract
- Produce card content, not HTML, CSS, SVG, or layout instructions.
- Keep the first result share-safe. Avoid secrets, tokens, private URLs, long absolute paths, raw tool outputs, and embarrassing private text unless the user asked for private analysis.
- Concrete numbers, exact times, quotes, and verdicts need evidence. If you cannot support a detail, omit it or soften it.
- Memory is prior notes, not ground truth. If a card uses a memory conclusion,
say in prose that it came from a prior memory when answering in chat, and add
memory_idevidence when producing JSON. - Do not propose a durable memory just because you generated a recap. Propose
memory only if the retrieval reveals a reusable cross-session conclusion not
already covered by
memories().
Retrieval Shape
Start with orientation and bounded evidence. Use English memory queries even when the recap text will be in another language.
const period = {
label: 'Week 24',
after: '2026-06-08T00:00:00+08:00',
before: '2026-06-15T00:00:00+08:00',
timezone: 'Asia/Shanghai',
};
const map = overview({ limit: 8 });
const project = map.current.project?.project;
const scoped = project ? { project } : {};
return {
current: map.current,
current_project: map.current_project,
prior_memories: memories({
...scoped,
query: 'weekly recap project decisions workflows debugging shipping',
limit: 8,
}).map(m => ({
id: m.id,
path: m.path,
anchors: m.anchors,
session_id: m.session_id,
created_at: m.created_at,
summary: m.summary?.slice(0, 280),
})),
sessions: sessions({
...scoped,
after: period.after,
before: period.before,
limit: 40,
}).map(s => ({
id: s.id,
title: s.title,
project: s.project,
branch: s.git_branch,
started_at: s.started_at,
ended_at: s.ended_at,
message_count: s.message_count,
})),
summaries: summaries({
...scoped,
after: period.after,
before: period.before,
limit: 30,
}).map(s => ({
id: s.id,
session_id: s.session_id,
session_title: s.session_title,
project: s.project,
timestamp: s.timestamp,
snippet: s.content?.slice(0, 260),
})),
workflows: workflows({
...scoped,
after: period.after,
before: period.before,
limit: 20,
}).map(w => ({
run_id: w.run_id,
session_id: w.session_id,
name: w.workflow_name,
status: w.status,
agent_count: w.agent_count,
tokens: w.total_tokens,
timestamp: w.timestamp,
})),
failures: failures({
...scoped,
after: period.after,
before: period.before,
limit: 12,
}).map(f => ({
session_id: f.session_id,
session_title: f.session_title,
tool: f.tool_name,
timestamp: f.timestamp,
snippet: f.content?.slice(0, 180),
})),
};
Use SQL for exact aggregate numbers after the helper-first pass has established the scope. Keep meta rows out of ordinary user-visible counts.
const project = '%quiet-zero%';
const after = '2026-06-08T00:00:00+08:00';
const before = '2026-06-15T00:00:00+08:00';
const metrics = sql(`
SELECT
COUNT(DISTINCT s.id) AS sessions,
COUNT(m.uuid) AS messages,
COALESCE(SUM(COALESCE(m.input_tokens, 0) + COALESCE(m.output_tokens, 0)), 0) AS tokens,
COUNT(DISTINCT substr(m.timestamp, 1, 10)) AS active_days
FROM sessions s
LEFT JOIN messages m ON m.session_id = s.id
WHERE s.project LIKE ?
AND COALESCE(m.is_meta, 0) = 0
AND m.timestamp >= ?
AND m.timestamp < ?
`, project, after, before)[0];
const user_messages = sql(`
SELECT
m.uuid,
m.session_id,
s.title AS session_title,
m.timestamp,
substr(m.text, 1, 500) AS text
FROM messages m
JOIN sessions s ON s.id = m.session_id
WHERE s.project LIKE ?
AND m.type = 'user'
AND m.content_type = 'text'
AND COALESCE(m.is_meta, 0) = 0
AND m.timestamp >= ?
AND m.timestamp < ?
ORDER BY m.timestamp
LIMIT 300
`, project, after, before);
return { metrics, user_messages };
Archetypes
Choose one dominant archetype from the evidence. Do not force all cards to match it; the archetype is the cover persona and tone baseline.
| archetype | essential action | tone baseline |
|---|---|---|
architect |
establishes structure, boundaries, and systems from above | matter-of-fact, structural pride |
debugger |
loops through symptoms until root cause becomes visible | wry, weary, occasional dark humor |
shipper |
keeps pushing forward with dense cadence | energetic, slightly breathless |
curator |
collects, organizes, refines, and distills | reflective, low-key clarity |
director |
coordinates many threads from the center outward | observant, slight remove |
cartographer |
reorganizes known structure, redraws maps, moves boundaries | patient, surveyor-like |
wanderer |
crosses projects without one obvious center, leaving traces | gentle, accepting, no apology |
Selection hints:
- Use
architectwhen the week is dominated by system design, APIs, schemas, boundaries, or core concepts. - Use
debuggerwhen repeated failures, false positives, regressions, or root cause hunts dominate. - Use
shipperwhen the evidence shows sustained implementation velocity, builds, releases, or many completed edits. - Use
curatorwhen the work is mostly cleanup, documentation, memory, organization, or taste/refinement. - Use
directorwhen workflows, subagents, review loops, or multi-agent coordination are the main story. - Use
cartographerwhen the user moves modules, redraws information architecture, re-scopes boundaries, or turns "this lives here" into "this belongs there". - Use
wandererwhen the period spans many unrelated projects and the honest story is exploratory rather than centered.
JSON Shape
The schema is deliberately card-content oriented. Keep keys stable, but let strings carry the style. Card text can use the user's language; field names stay English.
For weekly recaps, metrics.active_days and cover activity should contain 7
numbers ordered Monday through Sunday, where 0 means no visible activity and
1 means active. For monthly recaps, use one value per calendar day in the
period, or omit the field if the evidence is too thin.
type Recap = {
schema_version: "obelisk.recap.v1";
kind: "weekly" | "monthly";
generated_at: string;
period: {
label: string;
start: string;
end: string;
timezone: string;
};
source: {
project?: string;
session_ids: string[];
memory_ids?: string[];
};
metrics: {
sessions?: number;
messages?: number;
tokens?: number;
active_days?: number[];
streak_days?: number;
workflows?: number;
workflow_agents?: number;
corrections?: number;
};
persona: {
archetype:
| "architect"
| "debugger"
| "shipper"
| "curator"
| "director"
| "cartographer"
| "wanderer";
title: string;
subtitle: string;
tone: string;
};
cards: [
CoverCard,
ThinkingPathCard,
VibeCard,
WorkflowOrToolsCard,
ClosingCard
];
evidence?: Evidence[];
};
Card 1: Cover
type CoverCard = {
type: "cover";
badge: string;
title: string;
subtitle: string;
activity: number[];
footer: string;
evidence_refs?: string[];
};
Use the cover to name the period and the persona. The title can be an archetype label such as "The Architect"; the subtitle should summarize the period's real dominant work in one line.
Card 2: Thinking Path
type ThinkingPathCard = {
type: "thinking_path";
title: string;
items: Array<{
day: string;
prompt: string;
outcome: string;
evidence_refs?: string[];
}>;
};
Pick 3-6 turning points. A prompt is the question, friction, or task that
started the path. An outcome is the decision, fix, or learned framing.
Card 3: Vibe
type VibeCard = {
type: "vibe";
title: string;
observations: Array<{
label: string;
text: string;
count?: number;
time?: string;
evidence_refs?: string[];
}>;
meter?: {
label: string;
value: number;
caption: string;
};
quote?: {
text: string;
caption?: string;
evidence_refs?: string[];
};
};
This card can be playful, but it must stay grounded. Repeated phrases should be counted from visible user messages, not meta messages or tool results.
Card 4: Workflows Or Tools
type WorkflowOrToolsCard = {
type: "workflow" | "tool_habits" | "debugging" | "shipping";
title: string;
summary?: string;
stats?: string;
items: Array<{
name: string;
outcome: string;
evidence_refs?: string[];
}>;
verdict: string;
};
Choose the card type that best fits the period. Use workflow when workflow
runs or subagents are the story; debugging when failures and fixes dominate;
shipping when completed implementation dominates; tool_habits when the
period is mostly about how the user worked.
Card 5: Closing
type ClosingCard = {
type: "closing";
headline: string;
stats: string[];
most_said_phrase?: string;
signoff: string;
evidence_refs?: string[];
};
Close with one high-signal stat and a short signoff. Avoid turning the closing card into a second summary.
Evidence
type Evidence = {
id: string;
session_id?: string;
message_uuid?: string;
memory_id?: string;
summary?: string;
};
Evidence is for local traceability and app inspection. It does not have to be shown on exported cards. Prefer short summaries over raw snippets.
Output Rules
- If the user asks for app handoff, return one JSON object and no surrounding prose.
- If the user asks conversationally, answer with the card content naturally and include JSON only if useful.
- Keep
cards.length === 5in the order above. - Do not include unsupported cards just to fill space. It is better to make a quieter card than to invent drama.
- Do not include raw SQL, query scripts, or private evidence in the shareable card text.
Minimal Example
{
"schema_version": "obelisk.recap.v1",
"kind": "weekly",
"generated_at": "2026-06-13T03:40:00+08:00",
"period": {
"label": "Week 24",
"start": "2026-06-08",
"end": "2026-06-14",
"timezone": "Asia/Shanghai"
},
"source": {
"project": "quiet-zero",
"session_ids": ["sid-a", "sid-b"],
"memory_ids": ["mem-a"]
},
"metrics": {
"sessions": 12,
"messages": 847,
"tokens": 2400000,
"active_days": [1, 1, 1, 0, 1, 1, 1],
"workflows": 3,
"workflow_agents": 42
},
"persona": {
"archetype": "architect",
"title": "The Architect",
"subtitle": "Designed a memory system from raw sessions to durable notes.",
"tone": "matter-of-fact, structural pride"
},
"cards": [
{
"type": "cover",
"badge": "Week 24",
"title": "The Architect",
"subtitle": "Designed a full memory layer without turning sessions into a wiki.",
"activity": [1, 1, 1, 0, 1, 1, 1],
"footer": "12 sessions - 2.4M tokens",
"evidence_refs": ["e1", "e2"]
},
{
"type": "thinking_path",
"title": "Your thinking path",
"items": [
{
"day": "Mon",
"prompt": "Why compile sessions into a wiki?",
"outcome": "Kept raw SQLite as the evidence layer.",
"evidence_refs": ["e1"]
}
]
},
{
"type": "vibe",
"title": "Your vibe this week",
"observations": [
{
"label": "Catchphrase",
"text": "This is too ugly.",
"count": 4,
"evidence_refs": ["e3"]
}
],
"meter": {
"label": "Patience",
"value": 0.8,
"caption": "saint"
}
},
{
"type": "workflow",
"title": "Are you a Workflow Enjoyer?",
"stats": "3 workflows - 42 agents",
"items": [
{
"name": "hono-plugin-review",
"outcome": "perfect",
"evidence_refs": ["e4"]
}
],
"verdict": "Mostly tolerated"
},
{
"type": "closing",
"headline": "19 day streak",
"stats": ["847 messages exchanged"],
"most_said_phrase": "Okay, start doing it.",
"signoff": "See you next week.",
"evidence_refs": ["e5"]
}
],
"evidence": [
{
"id": "e1",
"session_id": "sid-a",
"message_uuid": "msg-a",
"summary": "The user chose raw SQLite as the evidence layer."
}
]
}