Add tracks — auto-updating note blocks with scheduled and event-driven triggers

Track blocks are YAML-fenced sections embedded in markdown notes whose output
is rewritten by a background agent. Three trigger types: manual (Run button or
Copilot), scheduled (cron / window / once with a 2 min grace window), and
event-driven (Gmail/Calendar sync events routed via an LLM classifier with a
second-pass agent decision). Output lives between <!--track-target:ID-->
comment markers that render as editable content in the Tiptap editor so users
can read and extend AI-generated content inline.

Core:
- Schedule and event pipelines run as independent polling loops (15s / 5s),
  both calling the same triggerTrackUpdate orchestrator. Events are FIFO via
  monotonic IDs; a per-track Set guards against duplicate runs.
- Track-run agent builds three message variants (manual/timed/event) — the
  event variant includes a Pass 2 directive to skip updates on false positives
  flagged by the liberal Pass 1 router.
- IPC surface: track:run/get/update/replaceYaml/delete plus tracks:events
  forward of the pub-sub bus to the renderer.
- Gmail emits per-thread events; Calendar bundles a digest per sync.

Copilot:
- New `tracks` skill (auto-generated canonical schema from Zod via
  z.toJSONSchema) teaches block creation, editing, and proactive suggestion.
- `run-track-block` tool with optional `context` parameter for backfills
  (e.g. seeding a new email-tracking block from existing synced emails).

Renderer:
- Tiptap chip (display-only) opens a rich modal with tabs, toggle, schedule
  details, raw YAML editor, and confirm-to-delete. All mutations go through
  IPC so the backend stays the single writer.
- Target regions use two atom marker nodes (open/close) around real editable
  content — custom blocks render natively, users can add their own notes.
- "Edit with Copilot" seeds a chat session with the note attached.

Docs: apps/x/TRACKS.md covers product flows, technical pipeline, and a
catalog of every LLM prompt involved with file+line pointers.
This commit is contained in:
Ramnique Singh 2026-04-14 13:51:45 +05:30 committed by GitHub
parent ab0147d475
commit e2c13f0f6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 3405 additions and 2 deletions

View file

@ -9,6 +9,7 @@ export * as agentScheduleState from './agent-schedule-state.js';
export * as serviceEvents from './service-events.js'
export * as inlineTask from './inline-task.js';
export * as blocks from './blocks.js';
export * as trackBlock from './track-block.js';
export * as frontmatter from './frontmatter.js';
export * as bases from './bases.js';
export { PrefixLogger };

View file

@ -6,6 +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-block.js';
import { UserMessageContent } from './message.js';
import { RowboatApiConfig } from './rowboat-account.js';
import { ZListToolkitsResponse } from './composio.js';
@ -193,6 +194,10 @@ const ipcSchemas = {
req: ServiceEvent,
res: z.null(),
},
'tracks:events': {
req: TrackEvent,
res: z.null(),
},
'models:list': {
req: z.null(),
res: z.object({
@ -560,6 +565,67 @@ const ipcSchemas = {
response: z.string().nullable(),
}),
},
// Track channels
'track:run': {
req: z.object({
trackId: z.string(),
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
summary: z.string().optional(),
error: z.string().optional(),
}),
},
'track:get': {
req: z.object({
trackId: z.string(),
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
// Fresh, authoritative YAML of the track block from disk.
// Renderer should use this for display/edit — never its Tiptap node attr.
yaml: z.string().optional(),
error: z.string().optional(),
}),
},
'track:update': {
req: z.object({
trackId: z.string(),
filePath: z.string(),
// Partial TrackBlock updates — merged into the block's YAML 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({
trackId: 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({
trackId: z.string(),
filePath: z.string(),
}),
res: z.object({
success: z.boolean(),
error: z.string().optional(),
}),
},
// Billing channels
'billing:getInfo': {
req: z.null(),

View file

@ -0,0 +1,87 @@
import z from 'zod';
export const TrackScheduleSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('cron').describe('Fires at exact cron times'),
expression: z.string().describe('5-field cron expression, quoted (e.g. "0 * * * *")'),
}).describe('Recurring at exact times'),
z.object({
type: z.literal('window').describe('Fires at most once per cron occurrence, only within a time-of-day window'),
cron: z.string().describe('5-field cron expression, quoted'),
startTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe('24h HH:MM, local time'),
endTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe('24h HH:MM, local time'),
}).describe('Recurring within a time-of-day window'),
z.object({
type: z.literal('once').describe('Fires once and never again'),
runAt: z.string().describe('ISO 8601 datetime, local time, no Z suffix (e.g. "2026-04-14T09:00:00")'),
}).describe('One-shot future run'),
]).describe('Optional schedule. Omit entirely for manual-only tracks.');
export type TrackSchedule = z.infer<typeof TrackScheduleSchema>;
export const TrackBlockSchema = z.object({
trackId: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe('Kebab-case identifier, unique within the note file'),
instruction: z.string().min(1).describe('What the agent should produce each run — specific, single-focus, imperative'),
eventMatchCriteria: z.string().optional().describe('When set, this track participates in event-based triggering. Describe what kinds of events should consider this track for an update (e.g. "Emails about Q3 planning"). Omit to disable event triggers — the track will only run on schedule or manually.'),
active: z.boolean().default(true).describe('Set false to pause without deleting'),
schedule: TrackScheduleSchema.optional(),
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),
});
// ---------------------------------------------------------------------------
// Knowledge events (event-driven track triggering pipeline)
// ---------------------------------------------------------------------------
export const KnowledgeEventSchema = z.object({
id: z.string().describe('Monotonically increasing ID; also the filename in events/pending/'),
source: z.string().describe('Producer of the event (e.g. "gmail", "calendar")'),
type: z.string().describe('Event type (e.g. "email.synced")'),
createdAt: z.string().describe('ISO timestamp when the event was produced'),
payload: z.string().describe('Human-readable event body, usually markdown'),
targetTrackId: z.string().optional().describe('If set, skip routing and target this track directly (used for re-runs)'),
targetFilePath: z.string().optional(),
// Enriched on move from pending/ to done/
processedAt: z.string().optional(),
candidates: z.array(z.object({
trackId: z.string(),
filePath: z.string(),
})).optional(),
runIds: z.array(z.string()).optional(),
error: z.string().optional(),
});
export type KnowledgeEvent = z.infer<typeof KnowledgeEventSchema>;
export const Pass1OutputSchema = z.object({
candidates: z.array(z.object({
trackId: z.string().describe('The track block identifier'),
filePath: z.string().describe('The note file path the track lives in'),
})).describe('Tracks that may be relevant to this event. trackIds are only unique within a file, so always return both fields.'),
});
export type Pass1Output = z.infer<typeof Pass1OutputSchema>;
// Track bus events
export const TrackRunStartEvent = z.object({
type: z.literal('track_run_start'),
trackId: z.string(),
filePath: z.string(),
trigger: z.enum(['timed', 'manual', 'event']),
runId: z.string(),
});
export const TrackRunCompleteEvent = z.object({
type: z.literal('track_run_complete'),
trackId: z.string(),
filePath: z.string(),
runId: z.string(),
error: z.string().optional(),
summary: z.string().optional(),
});
export const TrackEvent = z.union([TrackRunStartEvent, TrackRunCompleteEvent]);
export type TrackBlock = z.infer<typeof TrackBlockSchema>;
export type TrackEventType = z.infer<typeof TrackEvent>;