Symptom: scheduler logs `[heartbeat] Waking agent X (interval: 120s)`
every 15 seconds for three ICs, but no `[heartbeat] Starting taskless
heartbeat run` ever follows. Agents are wedged, UI shows nothing, and
the operator has no way to see why.
The loop had three compounding flaws:
1. `startAgentHeartbeatRun` returned `string | null`. Every gate
rejection was a silent `return null` — most were `logger.debug`,
which prod log levels filter out. Neither the scheduler nor the
renderer could tell the difference between "ran fine" and
"blocked by budget / no tasks / pending approval." The function
name promised "run or not" and the enum of reasons lived in
comments, not in the type.
2. The scheduler logged `[heartbeat] Waking agent ${name}` BEFORE
calling start, so even pure no-ops spammed the log. The "wake"
wasn't actually a wake; it was an attempt.
3. `markHeartbeatAt` is only called when a run starts. When start
returns null, `lastHeartbeatAt` stays frozen. On the next 15s
tick, `now - lastHeartbeatAt >= intervalMs` is still true, and
the scheduler fires again. The loop was a fixed-point at "skip →
log → skip → log" running 8 times per minute until something
external changed.
Root fix (data structure + visibility):
- `startAgentHeartbeatRun` now returns a discriminated union:
`{ status: "started"; runId } | { status: "skipped"; reason }`,
with `HeartbeatSkipReason` closed over 16 named reasons. Every
exit path carries its reason. The type tells operators what the
function can actually do; the enum tells them what it did.
- Scheduler holds a per-agent `attemptedAtByAgent` map. The interval
check uses `max(lastHeartbeatAt, lastAttemptedAt)`, so a streak of
skips still respects the heartbeat interval. Failed attempts cool
down the same as successful runs.
- On `skipped`, the scheduler persists `{reason, at}` into
`agents.runtime_state_json` under `heartbeatSkip`. On `started`,
it clears the field. The UI can read per-agent skip state from
the snapshot without a new IPC or table; operators finally see
"agent waiting: no_actionable_tasks" instead of a blank screen.
- The `Waking agent` log is gone. Its replacement prints once per
actual skip, with the reason inline (`[heartbeat] X not ready:
no_actionable_tasks`), and the existing `Starting taskless
heartbeat run for X` keeps marking the happy path.
`wakeAgentIfPossible` keeps its external `string | null` contract;
only the internal call site adapts. No caller outside of
`heartbeat.ts` / `scheduler-heartbeat.ts` / `scheduler.ts` needed
changes.
File sizes: heartbeat.ts 290, scheduler-heartbeat.ts 133. All under
the 400 cap. Tests: 603/603 green. Typecheck clean.
AgentCompany
Run an AI company from your desktop.
What it does
Type a company name and a goal. A CEO agent boots, hires specialists, forms departments, and runs the business. You approve or steer from the desktop.
Local, SQLite-backed, no external services. Inspired by Paperclip.
26 providers, no CLI
The runtime links pi-mono directly.
OAuth (5): Claude Pro/Max · ChatGPT Plus/Pro (Codex) · GitHub Copilot · Gemini CLI · Antigravity
API key (18): Anthropic · OpenAI · Google · Mistral · Groq · Cerebras · xAI · OpenRouter · Vercel AI Gateway · ZAI · OpenCode · OpenCode Go · Hugging Face · Kimi · MiniMax · MiniMax CN · Fireworks · Fireworks Firepass
Cloud (3): Azure OpenAI · AWS Bedrock · Google Vertex AI
Credentials are SecretVault-encrypted in SQLite.
Two execution modes
- AI CEO — autonomous. Hiring, tasks, deliverables run without you.
- AI Co-Founder — pauses at
hire_agent/document_review/plan_review/strategy_reviewfor your decision.
Switch at Settings → Execution mode.
Features
Self-organizing teams
CEO builds the org chart from one goal.
Agent messaging
Threads and DMs between agents. Messages land in each recipient's transcript as pi AgentMessage turns.
Tasks
Goals break into tasks. Agents pick them up, report progress, escalate blockers.
Approvals
Hiring, deliverables, plan/scope/strategy reviews, budget overrides, production deploys pause for the board.
Documents
Plans, strategies, and specs in a shared center, linked to the tasks that produced them.
Launch in a minute
Name, mission, goal, provider, mode.
Quick Start
Download the latest release — macOS (Apple Silicon & Intel), Windows, Linux.
Build from source
# Node.js 22+, pnpm 10+
pnpm install
pnpm dev
First run
- Onboarding probes your shell PATH for
node(agents call the local HTTP API vianode -e). - Settings → Providers. Sign in or drop a key.
- Create a company: name, mission, goal, mode, workspace.
Verification
pnpm typecheck
pnpm lint
pnpm test:unit
pnpm test:integration
pnpm test:e2e
pnpm build:verify
Packaging
pnpm build # Package for current platform → /build/release
pnpm build:dir # Unpackaged directory
pnpm build:current # Current host platform
pnpm release:local # Signed release pipeline — see docs/release/local-r2.md
Targets: macOS .dmg, Windows .exe (NSIS), Linux .AppImage. Built with electron-builder. Local builds use ad-hoc signing (CSC_IDENTITY_AUTO_DISCOVERY=false). Defaults to --publish never.
Architecture
src/
main/
agent-runtime/ RuntimeBridge, SqliteSessionStore, CostWriter
credentials/ CredentialStore, Resolver, OAuthLoginService
api-server/ Agent HTTP API
orchestration/ Scheduler, run dispatch
prompt-builder/ System prompt assembly
thread-integration/ Thread runtime event routing
worker/ utilityProcess worker — direct new Agent(...)
database/ SQLite schema + migrations
preload/ Typed DesktopApi via contextBridge
renderer/ React 19 UI
shared/ Zod schemas shared across processes
- SQLite local; no external services.
contextIsolation: true,nodeIntegration: false, Zod-validated IPC.- Worker constructs
new Agent({...})from@mariozechner/pi-agent-coredirectly. - Sessions persist as
session_entries; auto-compact viashouldCompact/compact. - Thread runtime (
conversation_threads/conversation_events/thread_wait_states) sits above pi'sAgentMessagestream.
Tech Stack
| Layer | Tech |
|---|---|
| Framework | Electron 39.8.0 |
| Language | TypeScript 5.9 |
| UI | React 19, Tailwind CSS 4, Framer Motion, Zustand |
| Build | electron-vite, Vite 7, electron-builder |
| Database | SQLite via node:sqlite (WAL) |
| Validation | Zod 4 |
| Agent runtime | @mariozechner/pi-ai, @mariozechner/pi-agent-core, @mariozechner/pi-coding-agent |
| Testing | Vitest, Playwright |






