The Co-Founder beforeToolCall hook (introduced in 77f81a7) gated every
non-readonly pi tool call as a `dangerous_command` approval. Because
agents call `POST /api/companies/:id/messages`, `/agents` (hire), and
every other company mutation via bash→node/curl, the first bash call in
every Co-Founder run got parked on a 30-minute approval timeout. Users
observed it as "agents never talk to each other" and "every bash command
waits for approval"; both symptoms are the same bug.
The business-level approval flow (`hire_agent`, `document_review`,
`plan_review`, etc. in execution-policy.ts) already gates the events
users actually care about, at the HTTP route layer where the event
originates (routes-agents.ts:368 for hiring, document routes for
deliverables). Stacking a pi-level hook on top was duplicate gating with
the wrong taxonomy.
Removed:
- src/main/agent-runtime/approval-hook.ts (entire file, 137 LOC)
- `beforeToolCall` conditional in worker-runner.ts and the
corresponding RPC method in worker-types.ts and runtime-bridge.ts
- `approvalHook` field + `resolveApproval` callback + shutdown
`cancelAll` in runtime-bridge.ts / service-graph-pi-runtime.ts /
wire-api-server.ts
Unchanged:
- `dangerous_command` approval type itself (still used by automation
workflows and routes-misc.ts)
- CEO mode (already never attached the hook)
- All business-level approval types and their wiring
Cost was written through three independent paths: worker-runner shipped
`finalUsage.costUsd` in run-finished -> finishRun wrote `runs.cost_usd`,
`tasks.cost_usd += costUsd`, `agents.spent_monthly_usd += costUsd`, and
inserted a per-run aggregate row in cost_entries. CostWriter
additionally inserted per-message rows in cost_entries from main. Any
CostWriter failure created silent drift between `SUM(cost_entries)`
(read by budget enforcement) and `runs.cost_usd` (read by the UI).
Make CostWriter the single writer, finishRun the single reader.
- Add `db.recordCostEntry(...)` as the first-class insert path for
per-message cost rows. CostWriter calls it; tests call it directly.
- Remove the aggregate INSERT from finishRun. finishRun now reads
`SELECT COALESCE(SUM(amount_usd), 0) FROM cost_entries WHERE run_id=?
AND unattributed=0` and writes that as the authoritative run total to
runs.cost_usd, tasks.cost_usd, and agents.spent_monthly_usd.
- Drop `costUsd` from `finishRun` input, the worker `run-finished`
ChildMessage shape, the automation `RunFinishedEvent` type, and every
caller + test fixture. Cost is owned by the per-message path.
- worker-runner stops extracting `usage.cost.total` — token counts stay
for session accumulation, cost comes from pi-ai via CostWriter.
- run-finished-helpers now gates budget incident checks on
`event.status === "succeeded"` instead of a cost field that no longer
exists on the event.
- automation-run-completion fires `budget_threshold` on every succeeded
run (post-run spent_monthly_usd already reflects the SUM path).
- Update budget tests to write a `recordCostEntry` before `finishRun`
to simulate the per-message cost path; drop `costUsd` from cross-
subsystem, edge-case-budget, database, and related fixtures.
One writer, one reader, one source of truth. No drift.
An earlier cleanup deleted HumanApprovalHook wholesale on the rationale
that the body always returned {}. That was the stub implementation, not
the feature: 版本更新.md specifies AI Co-Founder mode as "human-reviewed
execution that pauses until human review arrives". With no beforeToolCall
attached, Co-Founder and CEO modes behaved identically — a silent
regression of the product's core differentiator.
Bring the hook back with real gating logic and split the Agent
construction path by execution mode so CEO runs pay zero IPC overhead.
- HumanApprovalHook.before(): create a `dangerous_command` approval row
for non-whitelisted tool calls, emit approval-updated, return a promise
that resolves when a human decides.
- AUTO_APPROVED_TOOLS whitelist: read/ls/glob/grep/find/tree. Anything
else (bash, write, edit, …) waits for human approval.
- approvalHook.resolveApproval(): invoked from wire-api-server's
onApprovalResolved *before* automation and thread follow-ups, so the
pi Agent unblocks the instant the human decides.
- approvalHook.cancelAll(): release every pending wait on shutdown so the
worker can drain cleanly; wired into service-graph-pi-runtime.release.
- worker-runner: attach beforeToolCall only when job.executionMode ===
"ai_cofounder". CEO mode constructs Agent with no hook, no RPC hops.
- WorkerRpcClient.invoke: accept per-call timeoutMs override so approval
RPCs can wait 30min instead of the 30s default.
- worker-types schema: restore beforeToolCall in the method enum.
- RuntimeBridge.handleRpc: restore beforeToolCall branch.
No changes to approval DB schema, approval lifecycle, or existing
automation/thread handling. CEO-mode runs retain zero-IPC-per-tool
behavior from the prior cleanup.
karpathy-guidelines pass — never guess at types, read the source.
Root cause of the silent "等待日志输出" Console:
1. runtime-bridge's projectAgentEventToLogChunk switched on event types
that never exist in pi. Real types per pi-mono-reference/packages/
agent/src/types.ts `AgentEvent`:
agent_start / agent_end / turn_start / turn_end
message_start / message_update / message_end
tool_execution_start / tool_execution_update / tool_execution_end
(NOT the `assistant_message_delta` / `tool_call_start` / `error` I
invented.) Every case therefore fell through to `default → null`,
so zero log chunks were emitted even when the agent was streaming
tokens.
Rewrote the switch to match the real union. Streaming assistant
text and thinking now route through `message_update` ->
`assistantMessageEvent.text_delta` / `thinking_delta` (see
pi-mono-reference/packages/ai/src/types.ts `AssistantMessageEvent`
for the inner protocol). Tool activity routes through
`tool_execution_start` / `tool_execution_end`.
2. worker-runner had no Console-visible breadcrumbs between
`run-status: running` and eventual turn output, so when the agent
stalled (credential fetch, model resolution, first API call) the
user saw a bare "RUNNING" with no clue where time was being spent.
Added 5 targeted `run-log` probes:
[runner] starting run ...
[runner] prepareRun ok, resolving model
[runner] model=... provider=... — constructing Agent
[pi-event] <type> (inside agent.subscribe, one per event)
[runner] calling agent.prompt (N chars)
[runner] agent.prompt resolved
These are structured runtime signals, not test/mockup data. They
surface the exact phase a stuck run is in.
typecheck 0 / lint 0-0 / test:unit 508/508 / build:verify 0-0.
pi-ai's getModel/getModels does not know about the Fireworks providers
(fireworks, fireworks-firepass); both live under our local model-resolver
because Fireworks speaks the Anthropic-compat endpoint with Bearer auth
at https://api.fireworks.ai/inference, not pi-ai's built-in catalog.
Three call sites were still hitting pi-ai directly and failing silently
for Fireworks:
1. connector-model-policy.ts — listProviderModels() caught the pi-ai
throw and returned []. The "Fire Pass (CEO runtime)" card in the
Team Models onboarding step therefore showed "暂无模型 / 已启用 0/0",
blocking users on Fire Pass from continuing onboarding.
2. worker/worker-runner.ts — getModel() threw mid-run when the selected
connector was Fireworks, so the worker could never actually execute
an agent turn against Fire Pass or the curated Fireworks model set.
Now uses resolveModel() + the Fireworks-specific Bearer header from
resolveExtraStreamHeaders() so streamSimple hits the correct endpoint
with the correct auth.
3. agent-runtime/cost-writer.ts — cost observation silently wrote
amount_usd=null for Fireworks runs because getModel() threw inside
the try/catch. Now resolves via resolveModel() so the Fireworks
curated contextWindow/maxTokens are visible to calculateCost().
Verification: pnpm typecheck 0 / pnpm lint 0-0 / pnpm build:verify 0-0
/ pnpm test:unit 508/508.
Rewrites the utility-process worker to drive pi-agent-core's Agent class
against pi-ai models and pi-coding-agent tools. The entry file keeps the
queue and cancellation state, delegates runs to WorkerRunner, and routes
RPC responses through WorkerRpcClient so beforeToolCall/afterToolCall
approvals and prepareRun credential resolution travel back to main's
RuntimeBridge. Worker message schema gains `agent-event` + `rpc-request`
envelopes, and run-finished no longer depends on the deleted sessionCodec.