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.
509 lines
14 KiB
Markdown
509 lines
14 KiB
Markdown
# 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_id` evidence 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.
|
|
|
|
```js
|
|
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.
|
|
|
|
```js
|
|
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 `architect` when the week is dominated by system design, APIs, schemas,
|
|
boundaries, or core concepts.
|
|
- Use `debugger` when repeated failures, false positives, regressions, or root
|
|
cause hunts dominate.
|
|
- Use `shipper` when the evidence shows sustained implementation velocity,
|
|
builds, releases, or many completed edits.
|
|
- Use `curator` when the work is mostly cleanup, documentation, memory,
|
|
organization, or taste/refinement.
|
|
- Use `director` when workflows, subagents, review loops, or multi-agent
|
|
coordination are the main story.
|
|
- Use `cartographer` when the user moves modules, redraws information
|
|
architecture, re-scopes boundaries, or turns "this lives here" into "this
|
|
belongs there".
|
|
- Use `wanderer` when 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.
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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 === 5` in 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
|
|
|
|
```json
|
|
{
|
|
"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."
|
|
}
|
|
]
|
|
}
|
|
```
|