feat: live notes — single objective per note replaces multi-track model

Folds the multi-`track:`-array model into one `live:` block per note: a single
persistent objective the live-note agent maintains, plus an optional triggers
object (`cronExpr` / `windows` / `eventMatchCriteria`, each independently
optional). A note is now passive or live — no per-track scopes, no section
ownership contract, no `once` trigger. The agent owns the whole body and makes
patch-style incremental edits per run.

Highlights:
- Schema: `track:` array → single `live:` object (`packages/shared/src/live-note.ts`).
- Runtime: scheduler / event processor / runner under `core/knowledge/live-note/`,
  with split `lastAttemptAt` (every run, drives 5-min backoff) vs `lastRunAt`
  (success only, anchors cycles). `throwOnError` on agent runs surfaces LLM /
  billing failures into `lastRunError`.
- Today.md: regenerated by template v2 (single objective covering overview /
  calendar / emails / what-you-missed / priorities; existing files renamed to
  `Today.md.bkp.<stamp>`).
- Renderer: `LiveNoteSidebar` mounts inside the editor row (no chat overlap,
  auto-closes on note switch); toolbar Radio button becomes a status pill;
  `LiveNotesView` replaces background-agents view.
- Copilot: new `live-note` skill with act-first stance, default folder/cadence
  pickers, and a non-negotiable rule to extend an existing objective rather
  than add a second one. Shared `KNOWLEDGE_NOTE_STYLE_GUIDE` enforces
  terse-and-scannable writing across `doc-collab` and the live-note agent.
- Analytics: `track_block` use-case → `live_note_agent`; trigger
  (`manual` / `cron` / `window` / `event`) becomes the Pass-2 sub-use-case,
  alongside `routing` for Pass 1. Legacy run files with the old value are
  read-mapped via `LegacyStartEvent` so they stay openable in the runs list.

Hard cutover — no back-compat shims for legacy `track:` frontmatter arrays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-05-09 00:26:46 +05:30
parent 0bf7a55611
commit dabca3da19
59 changed files with 3816 additions and 3212 deletions

View file

@ -6,7 +6,7 @@ import { LlmModelConfig } from './models.js';
import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js';
import { AgentScheduleState } from './agent-schedule-state.js';
import { ServiceEvent } from './service-events.js';
import { TrackEvent } from './track.js';
import { LiveNoteAgentEvent, LiveNoteSchema } from './live-note.js';
import { UserMessageContent } from './message.js';
import { RowboatApiConfig } from './rowboat-account.js';
import { ZListToolkitsResponse } from './composio.js';
@ -214,8 +214,8 @@ const ipcSchemas = {
req: ServiceEvent,
res: z.null(),
},
'tracks:events': {
req: TrackEvent,
'live-note-agent:events': {
req: LiveNoteAgentEvent,
res: z.null(),
},
'models:list': {
@ -611,93 +611,83 @@ const ipcSchemas = {
response: z.string().nullable(),
}),
},
// Track channels
'track:run': {
// Live-note channels
'live-note:run': {
req: z.object({
filePath: z.string(),
context: z.string().optional(),
}),
res: z.object({
success: z.boolean(),
runId: z.string().nullable().optional(),
action: z.enum(['replace', 'no_update']).optional(),
summary: z.string().nullable().optional(),
contentAfter: z.string().nullable().optional(),
error: z.string().optional(),
}),
},
'live-note:get': {
req: z.object({
id: z.string(),
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
summary: z.string().optional(),
// Fresh, authoritative live-note object from frontmatter, or null when
// the note is passive. Renderer should use this for display/edit —
// never a stale cached copy.
live: LiveNoteSchema.nullable().optional(),
error: z.string().optional(),
}),
},
'track:get': {
'live-note:set': {
req: z.object({
id: z.string(),
filePath: z.string(),
live: LiveNoteSchema,
}),
res: z.object({
success: z.boolean(),
// Fresh, authoritative YAML of the track from frontmatter.
// Renderer should use this for display/edit — never a stale cached copy.
yaml: z.string().optional(),
live: LiveNoteSchema.nullable().optional(),
error: z.string().optional(),
}),
},
'track:update': {
'live-note:setActive': {
req: z.object({
id: z.string(),
filePath: z.string(),
// Partial Track updates — merged into the entry on disk.
// Backend is the sole writer; avoids races with scheduler/runner writes.
updates: z.record(z.string(), z.unknown()),
}),
res: z.object({
success: z.boolean(),
yaml: z.string().optional(),
error: z.string().optional(),
}),
},
'track:replaceYaml': {
req: z.object({
id: z.string(),
filePath: z.string(),
yaml: z.string(),
}),
res: z.object({
success: z.boolean(),
yaml: z.string().optional(),
error: z.string().optional(),
}),
},
'track:delete': {
req: z.object({
id: z.string(),
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
error: z.string().optional(),
}),
},
'track:setNoteActive': {
req: z.object({
path: RelPath,
active: z.boolean(),
}),
res: z.object({
success: z.boolean(),
note: z.object({
path: RelPath,
trackCount: z.number().int().positive(),
createdAt: z.string().nullable(),
lastRunAt: z.string().nullable(),
isActive: z.boolean(),
}).optional(),
live: LiveNoteSchema.nullable().optional(),
error: z.string().optional(),
}),
},
'track:listNotes': {
'live-note:delete': {
req: z.object({
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
error: z.string().optional(),
}),
},
'live-note:stop': {
req: z.object({
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
error: z.string().optional(),
}),
},
'live-note:listNotes': {
req: z.null(),
res: z.object({
notes: z.array(z.object({
path: RelPath,
trackCount: z.number().int().positive(),
createdAt: z.string().nullable(),
lastRunAt: z.string().nullable(),
isActive: z.boolean(),
objective: z.string(),
})),
}),
},