Boss build seeds one Fire Pass key for every agent. Three concurrent
agents -> three concurrent streamSimple calls against the Kimi K2.6
Turbo router -> three HTTP 429s on the overview dashboard.
Add an in-memory async semaphore keyed by (provider, sha1(apiKey)) and
acquire a slot inside `createSafePiStream` before invoking the source.
The slot is released when the stream finishes, errors, or aborts.
`fireworks-firepass` is capped at 1 concurrent call; every other
provider stays uncapped (no fallback layer, no 429 retry — flow control
prevents the burst at the source instead of recovering after).
Same code path applies to public and boss builds: only the seeded key
differs, the gate behaviour is identical.
Both servers expose OpenAI-compatible /v1/chat/completions and /v1/models,
so we route them through pi-ai's `openai-completions` API the same way
Fireworks does — but with a per-installation base URL the user enters
during onboarding instead of a hardcoded one. The shared schemas grow a
`local` credential payload carrying just the baseUrl; the credential
resolver surfaces it on `ResolvedCredential.baseUrl`, the runtime bridge
forwards it through `prepareRun`, and the worker plumbs it into
`resolveModel` so the streamed Model<openai-completions> targets the
user's server. The local-cost path short-circuits to zero in cost-writer
since users bill their own hardware, and the live model list is fetched
from the saved baseUrl by `fetchLocalConnectorModels` (Ollama and LM
Studio both speak the standard OpenAI catalog shape). Onboarding ships
a new LocalProviderCard and the Settings panel a LocalForm; both default
to the catalog's per-connector localhost URL and accept any custom URL.
Fireworks rolled out the kimi-k2p6-turbo router as the new Fire Pass
target; the k2p5-turbo route will start refusing Fire-Pass-only
callers. Bump the router id and display name in connector-catalog,
apikey connector definition, model-resolver, and capability-seed so
the desktop catalog, the renderer's onboarding rail, the capability
matrix, and the runtime model resolver all point at the same model.
agent_message_reads is the per-viewer source of truth for readAt, but
the send path never recorded the author's own receipt. Every message
the board operator or an agent sent therefore counted as unread to
themselves until they happened to re-open the channel — sidebar badge,
inbox panel, and the channel sidebar all derive from the same join
table. Insert a (message_id, from_agent_id, created_at) row in the
same transaction as the message itself; the receipt becomes an
atomic part of the write rather than a deferred maintenance task.
handleRunFinished and releaseCompletedRunWorkspaceLock both run between
receiving run-finished and posting run-completion-ack. If either throws,
the ack is skipped and the worker keeps the agent in its `active` map
forever; every subsequent wake then queues behind an `isAgentBusy` check
that never clears, and manual wakes from the UI silently fail to start.
Wrap the post-finalize work in try/catch so the ack is posted
unconditionally and the worker state machine stays consistent.
Pi-ai ships $0 pricing for github-copilot (25 models), google-gemini-cli
(6 models), zai (13 models), and kimi-coding (2 models). This made cost
tracking produce $0 for every run through these providers, rendering
budget enforcement useless.
Added resolveModelCostFallback() in model-resolver.ts with two strategies:
1. Cross-provider lookup: subscription providers (github-copilot,
google-gemini-cli) use the same models as their API counterparts.
Look up the model in anthropic/openai/google/xai catalogs using
normalized ID matching (dots→hyphens) to find real pricing.
2. Per-provider fallback: API-key providers with no cross-provider
equivalent (zai, kimi-coding) get a static per-provider cost.
CostWriter now calls resolveModelCostFallback after resolveModel, so
all consumers get patched pricing without touching the pi-ai library.
Three fixes for the non-functional budget system:
1. Fireworks models hardcoded cost {input: 0, output: 0}, making all
Fireworks/Fire Pass runs report $0 and budget enforcement useless.
Added per-model pricing to FIREWORKS_CURATED_MODELS with a fallback
for unknown models. Pi-ai known providers (Anthropic, OpenAI, etc.)
already had correct per-model pricing via getModels().
2. saveAgent IPC handler passed the form's submitted status through
without checking whether the agent was budget-paused and the new
budget exceeds spent. Now auto-resolves paused → idle when the user
raises budget_monthly_usd above spent_monthly_usd.
3. checkAutoResolve in budget-enforcement-service resolved policy
incidents but never touched agents.status. Added
unpauseBudgetBlockedAgents() that scans for paused agents whose
legacy budget now has headroom and flips them to idle.
Agents always replied with channel="direct" regardless of the original
message's channel, creating new DM conversations instead of replying
in the existing group chat (department/company/project/incident).
Root cause: three layers of the system guided agents toward direct-only
replies:
1. Prompt instructions (wake-reasons, IC/manager operating instructions)
hardcoded channel="direct" and toAgentId=fromAgentId for all replies
2. Standup API omitted toAgentId from unread messages, removing context
about who the original message was addressed to
3. Inbox projection contained no reply-routing metadata, so the agent's
LLM had no signal about which channel to respond in
Fixes:
- routes-agents-standup.ts: include toAgentId in unread message payload
- wake-reasons/en.ts: differentiate direct vs group channel reply format
- operating-en.ts: same channel-preserving guidance for IC and manager
- inbox-projection.ts: embed reply-to routing context in message header
so the LLM sees channel + target alongside the message body
The heartbeat path (canRunAgent → startAgentHeartbeatRun) only checked
isAgentPendingApproval (hire gate), completely bypassing plan_review,
draft_review, strategy_review and other blocking approval types. Agents
in Co-Founder mode continued running despite pending approvals.
Root cause: task-runner.prepareTaskRun had the full getBlockingApproval
check, but heartbeat runs never went through prepareTaskRun — they
created runs directly, skipping the approval gate entirely.
Three fixes:
- heartbeat.canRunAgent: add getBlockingApproval(companyId, agentId, null)
for agent-level blocking approvals
- heartbeat.startAgentHeartbeatRun: add getBlockingApproval with task id
after task resolution, before run creation
- task-runner.resumeQueuedRuns: add getBlockingApproval check so app
restart does not dispatch runs past pending approvals
Also replaced the hardcoded type list in hasPendingReviewApprovalForAgent
with a dynamic join on approval_policies.blocks_execution, eliminating
the sync risk when new blocking approval types are added.