Moore machine baseline must update BEFORE role execution starts,
not after. This prevents duplicate dispatch when tick() is called
again while a slow role (e.g. Cursor ~60s) is still running.
Added test: 'baseline updates before execution' verifies that a
concurrent tick sees snapshot === baseline and skips.
The Moore machine should fire outputs only when STATE changes,
not when ACTIONS change. Compare JSON-serialized snapshot baseline
to determine if a new tick needs to run moderator + roles.
Covers TopicType interface (events, projection, roles, moderator),
Topic-as-Rule pattern, one-rule-per-type design, and Moore machine
diff-driven ticks with references to source files in topics/.
Made-with: Cursor
closes#24
- Delete stale `hello` test file
- Remove deprecated type aliases: TaskExecutingMeta, TaskAckedMeta,
TaskGivenUpMeta, ProjectPausedMeta, ProjectArchivedMeta (all `never`)
- Remove deprecated VitalRecord type alias (was just EventRecord)
- Remove deprecated AgentCapabilityStats interface and
buildAgentCapabilityStats function (returned empty object)
- Remove agent-capability-stats from rebuildSnapshot
- Clean up re-exports in index.ts and upulse/store.ts
- All 355 tests pass, 0 lint errors
Previously events.id was left null because the INSERT statement omitted
the id column. Now generates a ULID before each INSERT and passes it
as the first bind parameter. Also migrates the schema from
INTEGER PRIMARY KEY AUTOINCREMENT to TEXT PRIMARY KEY and updates
projections.last_event_id from INTEGER to TEXT accordingly.
Made-with: Cursor
After #118 homogeneous scope db refactoring, the events table id column
uses ULID TEXT in production DBs, but rowToRecord() still called
Number(row.id) which produced NaN → 0 for all events.
This caused executorLoop to skip all effects after the first one
(inflight.has(0) always true) and getLatest('effect-acked', '0')
matched wrong events.
Changes:
- EventRecord.id: number → string
- rowToRecord(): Number(row.id) → String(row.id)
- ProjectionState.lastEventId: number → string
- executorLoop inflight: Set<number> → Set<string>
- Remove redundant String() wrapping on effectEvent.id
- Update tests for string id comparisons
Made-with: Cursor
* feat(pulse): Task state machine redesign — pending/assigned/closed + broker executor
Redesign task lifecycle from 6-state (pending/assigned/executing/acked/
closed/given-up) to a cleaner 3-state machine (pending/assigned/closed)
with a task-responded event that cycles back to pending for re-brokering.
- Rewrite task-events.ts: new TaskType, TaskCreatedMeta with description/
type/priority/creatorId, TaskRespondedMeta, ProjectCreatedMeta with
required repoDir, ProjectState type
- Rewrite pending-tasks-projection.ts: adapt fold logic to new state
machine, add buildProjectsFromEvents and buildInflightBrokerFromEvents
- New rules/task-rule.ts: lightweight rule (no LLM) that emits broker
effects for pending tasks and cursor effects for assigned tasks
- New executors/broker.ts: LLM-based broker that assigns pending tasks
to agents via tool calling
- Rewrite cursor-agent.ts (pulse-cursor): CursorEffect now uses
kind/taskId/projectId instead of type/prompt/scenario/repoDir,
executor resolves task and project details from routineStore,
writes task-responded events instead of task-acked
- Update all tests for new state machine
Closes#119
* fix(task-state-machine): add routing status for broker inflight idempotency
Made-with: Cursor
---------
Co-authored-by: 小橘 <xiaoju@shazhou.work>
* refactor(pulse): homogeneous scope db — def tables in every scope + fix last_event_id type (closes#115, closes#117)
* refactor(pulse): homogeneous scope db — def tables in every scope + fix last_event_id type (closes#115, closes#117)
---------
Co-authored-by: 小墨 <xiaomooo@shazhou.work>
ULID's random part was non-monotonic within the same millisecond,
causing WHERE id > ? cursor queries to silently miss events.
This was the root cause of the flaky projection integration test.
Changes:
- events.id: TEXT PRIMARY KEY → INTEGER PRIMARY KEY AUTOINCREMENT
- Remove makeUlid() entirely
- appendEvent uses SQLite last_insert_rowid()
- projection-engine: lastEventId string → number (default 0)
- executorLoop: effectEvent.id → String() for key storage
- All 8 affected files updated, 319 tests pass (0 fail)
Breaking: EventRecord.id type changed from string to number.
Existing .db files need schema migration (recreate recommended).
macOS: use vm_stat to include inactive + purgeable pages as available.
Linux: use /proc/meminfo MemAvailable instead of MemFree.
Fallback to os.freemem() on other platforms.
Before: macOS reports ~95% on a 16GB machine with 7.5GB available.
After: correctly reports ~55%.
Also updated upulse init template so new installations get the fix.
- EventsPage: track currentLimit state, cap at MAX_EVENTS_LIMIT=500,
show 'All events loaded' or 'Limit reached (500)' states
- DeploysPage: add comment noting server-side 50/kind cap
— 小糯 🐱
server.ts is at src/ui/server.ts, so the dist path needs to go up
two levels (src/ui → src → package root) to reach ui-app/dist/.
Was going up only one level, causing hasDistDir=false and always
falling back to the old embedded DASHBOARD_HTML (no routing).
— 小糯 🐱
* feat(upulse): React WebUI dashboard with full data model coverage
Replace the embedded single-file HTML dashboard with a proper React app:
## Frontend (ui-app/)
- React 19 + Vite + Tailwind 4 + Radix UI (same stack as OGraph board)
- 6 pages: Dashboard, Events, Scopes, Projections, Definitions, Deploys
- Dark zinc theme, responsive sidebar navigation
- Auto-refresh on Dashboard (5s) and Events (10s)
- Click-to-expand JSON details throughout
## Backend (server.ts)
New API endpoints for the v2 data model:
- GET /api/scopes — list all scope names
- GET /api/scopes/:scope/events — events for a specific scope
- GET /api/scopes/:scope/projections — projection states per scope
- GET /api/defs/objects — object definitions
- GET /api/defs/events — event definitions
- GET /api/defs/projections — projection definitions with sources
Static file serving from ui-app/dist/ with SPA fallback.
Falls back to embedded HTML when dist/ doesn't exist.
All existing API endpoints preserved unchanged.
— 小糯 🐱
* fix: address review feedback on PR #107
1. Scope event count: /api/scopes now returns { name, eventCount } per scope
via SELECT COUNT(*) on each scope DB (no more imprecise client-side probing)
2. Build artifacts: added .gitignore for ui-app/ (dist/, node_modules/, *.tsbuildinfo)
3. API error handling: scope endpoints now return 503 when store unavailable,
404 when scope not found (was silently returning empty arrays)
— 小糯 🐱
---------
Co-authored-by: 鹿鸣 <luming@shazhou.work>
PR #80 changed pgrep to systemctl show --property=MainPID + ps,
but tests still mocked only one execSync call (the ps call).
Now mock systemctl call first, then ps call.
Ref #80
Co-authored-by: 小橘 <xiaoju@shazhou.work>
Phase 1 added buildPendingTasksFromEvents (correct event-fold approach).
Phase 2 wired it into rebuildSnapshot/runPulse.
Phase 3 removes the old polling workaround (pending-tasks.ts).
The polling workaround (pending-tasks.ts) was only on feat/task-dispatch-loop
and never merged to main. This commit cleans up the last reference to
pendingTasksWatcher in pending-tasks-projection.ts comments.
Ref #83
Made-with: Cursor
Co-authored-by: 小橘 <xiaoju@shazhou.work>
rebuildSnapshot now accepts an optional systemStore parameter; when
provided and senseKeys includes 'pending-tasks', buildPendingTasksFromEvents
and buildAgentCapabilityStats are automatically included in the snapshot
as 'pending-tasks' and 'agent-capability-stats' senseKeys.
runPulse automatically passes systemStore to rebuildSnapshot so that
task projections are available to rules without extra configuration.
Closes#89, ref #83
Made-with: Cursor
Co-authored-by: 小橘 <xiaoju@shazhou.work>