Post-recluster-schedule integration. Covers documentation gaps, architectural observations, and concrete improvement areas.
pulse/
async/ Job execution engine (queue, workers, budget gates, handler registry)
schedule/ Recurring job scheduler (store, ticker @ 1s, execution tracking)
budget/ Cost enforcement (daily/weekly/monthly limits, rate limiter)
progress.go Domain-agnostic progress interface
server/pulse_*.go REST API (schedules, jobs, executions, stages, task logs)
web/ts/pulse/ Frontend (22 modules — panels, cards, real-time handlers)
ID conventions: SP_* = scheduled job, JB_* = async job, execution IDs link the two.
Data flow: Schedule ticker → creates async job → worker picks up → handler executes → execution record + task_logs written → poller broadcasts completion → WebSocket → UI updates.
web/ts/pulse/ has 22 files, no README. Module boundaries, data flow, and real-time subscription model undocumented.task_logs table has stage/task_id columns used throughout the UI but nowhere defines what these represent.docs/api/pulse-*.md) — auto-generated stubs with endpoint names only. No request/response schemas, no examples.pulse-resource-coordination.md — design proposal (Issue #50), not implemented. Should be marked as such.All Pulse frontend modules now use apiFetch() from web/ts/api.ts. getBaseUrl() + raw fetch() and safeFetch() removed.
formatDuration, formatRelativeTime, escapeHtml consolidated in web/ts/html-utils.ts. Duplicate definitions in panel.ts and execution-api.ts removed.
s.newScheduleStore() / s.newExecutionStore() called ~13 times across pulse handlers. Lightweight but unnecessary — could be a field on QNTXServer.
Manual triggers via REST API go straight to the queue without rate limit check. Only budget is checked. The worker checks rate limits before execution, but the job is already queued.
pulse_execution_poller.go polls every 3 seconds with no jitter. Multiple servers could poll simultaneously. No backoff for expensive queries on large databases.
Creating a schedule with a nonexistent handler silently succeeds. Fails at execution time with "handler not found" — could be caught at creation.
| Location | Issue | Description |
|---|---|---|
ticker.go:120 | #478 | Health check: sync tree size vs attestation count |
worker.go:479 | #70 | System load gate (3rd gate before job execution) |
grace_test.go:143,489 | #71 | Executor injection during WorkerPool creation |
migration 003 | — | Rename created_from_doc_id → created_from |
realtime-handlers.ts:16 | #30 | Execution progress, cancellation, batch updates |
async.ts, schedule.ts | — | Migrate generated types to proto generation |
The server/pulse_*.go handlers access data three ways:
| Pattern | Files | Risk |
|---|---|---|
| Store abstraction | pulse_schedules.go, pulse_execution_handlers.go, pulse_execution_poller.go | Clean |
Raw SQL on task_logs | pulse_task_stages.gopulse_task_logs.go | ✅ Fixed — now uses TaskLogStore |
Raw SQL on pulse_executions | pulse_job_children.go | ✅ Fixed — now uses ExecutionStore.GetAsyncJobIDForScheduledJob |
| Raw transaction in handler | pulse_schedules.go:209-294 | ✅ Fixed — now uses Store.CreateForceTriggerExecution() |
s.db exposed to all handlersEvery handler receives *QNTXServer which has a public db field. No compile-time enforcement that handlers use stores.
Some handlers call s.newScheduleStore(), others call schedule.NewStore(s.db) directly. Stores are stateless so this is correct but inconsistent.
| Table | Store exists? | Gap |
|---|---|---|
scheduled_pulse_jobs | ✅ schedule.Store | — |
pulse_executions | ✅ schedule.ExecutionStore | — |
task_logs | ✅ schedule.TaskLogStore | Write path (embeddings_pulse.go) still uses raw INSERT |
async_ix_jobs | ✅ async.Queue (store.go) | — |
apiFetchhtml-utils.ts is canonicalschedule.TaskLogStore replaces raw SQL in pulse_task_stages.go and pulse_task_logs.goGetAsyncJobIDForScheduledJob to ExecutionStorepulse_job_children.goschedule.StoreStore.CreateForceTriggerExecution() replaces 85-line inline transaction3s ± 500ms prevents synchronized pollingembeddings_pulse.go write path uses raw SQL (2 duplicate INSERT statements)