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
The earlier sweep cleaned the hot path (worker/runtime/automation/api-
server/ipc core — 59 sites). This pass finishes the deferred long tail:
main/* services, database/* CRUD modules, api-server/* routes, ipc-*
handlers, thread-integration, automation helpers, browser, skills.
Default strategy: catch (error) { logger.warn("[module] context:", ...) }
— preserve current behavior, make the failure visible instead of
silently hidden. Widened 3 logger interface signatures to include warn
where callers only passed info/error previously (thread-helpers,
host-environment-service, ipc-domain-common).
Legitimate boundaries left intact: JSON.parse on untrusted input, DNS
resolver no-records paths, explicit safe* helpers (safeJson, parseJson,
parseTriggerDetail), URL-parse predicates, and `<unserialisable>`
fallbacks inside the prompt-builder summarisers. Each kept catch now
either has a reason in context or is a documented safe* helper.
Tests adjusted only where test-constructed no-op loggers needed a warn
stub (message-dispatch, company-mode-switch, ipc-agent-graph-handlers).
With the earlier pass, AgentCompany no longer has silently-swallowing
catch blocks in main-process or renderer business code.
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.
Inter-agent messages previously entered the recipient's context as a
separate prompt block (recentMessages), rebuilt every turn. This double-
charged tokens and kept messages outside pi's native AgentMessage stream.
Now at prepareRun: pull the recipient's unread inbox, project each
AgentMessageRecord into a pi user-role AgentMessage, persist to
SqliteSessionStore, and mark delivered. The recipient reads messages
as natural conversation turns in the transcript.
- db: listAgentInboxUnread + markAgentInboxDelivered (transactional)
- agent-runtime/inbox-projection.ts: AgentMessageRecord -> pi AgentMessage
- runtime-bridge.prepareRun: inject + deliver before loadMessages
- prompt-builder: delete recentMessages context block + buildMessagesSection
- message-normalize: drop unused normalizeMessagesForPrompt
- delete prompt-builder/attachment-resolver.ts (orphan after section removal)
Send path (api-server/routes-messages, ipc, UI) unchanged — inbox table
remains the UI source. Transactional delivery marking prevents duplicate
injection across runs. Failed append -> no mark -> retry on next run.
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.
Two coupled user-visible defects were fixed.
1. Every tool call (shell / edit / write / read / ...) triggered a
synchronous human-approval prompt under BOTH execution modes. The
worker blocked inside pi's beforeToolCall waiting on a DB approval
row, so runs never progressed — "等待日志输出" forever.
The execution-mode contract is now enforced:
- AI CEO → fully autonomous, no framework gate.
- AI Co-Founder → still fully autonomous at the pi layer. Human
review is a model-driven decision (LLM calls an explicit
`request_approval` tool when it judges review necessary), not
a framework-level interrupt.
HumanApprovalHook collapses to a no-op `before` / `after` /
`cancelAll`. All machinery that queued `dangerous_command`
approvals from the worker-side hook is removed. No more starving
the agent on `node -e` self-call checks.
2. The Console panel only subscribed to `run-log` stdout/stderr
chunks, so pi's agent-event stream (assistant_message_delta,
tool_call_start/end, errors, turn_end) produced zero visible
output. With bug (1) fixed, runs now execute — but the user
would still see a blank Console.
runtime-bridge.handleAgentEvent now projects each meaningful
agent-event into a run-log chunk (assistant_message_delta → raw
delta, tool_call_start → `[tool:bash] {args}`, tool_call_end →
`[tool:done|error:name] {result}`, error → stderr). ConsoleView
picks these up via the existing run-log DesktopEvent pipe with
no renderer change.
tests/unit/approval-hook.test.ts rewritten to assert the new
autonomous contract (before always returns {}, no approval rows
written, cancelAll is a no-op). Tests 508/508 pass.
typecheck 0 / lint 0-0 / build:verify 0-0.
Bootstraps CredentialStore/Resolver, OAuthLoginService, and RuntimeBridge
inside service-graph via the existing pi-runtime factory, and exposes the
bridge to worker-service for agent-event + rpc-request routing. Adds a
`prepareRun` RPC that resolves credentials, loads prior messages, and
registers the active run context in a single call. Session rotation drops
the adapter nativeContextManagement branch; run payloads now carry the
taskSessionId and executionMode the worker needs to drive pi-agent-core.
P3-56: three call sites repeated the same 'lookup → fall back to
new → set if missing' dance for session stores. The duplication hid the
fact that the fallback allocation in the second and third sites could
be wasted when the entry already existed. Extract a single
getOrCreateStore helper that does one map lookup, one conditional
construction, and always returns the cached instance.