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.
Migrate all remaining pass-through call sites (social, skills, budget,
entityCrud, documents, knowledge, automation, wakeup, backup, threads,
settings, snapshot, connectors, workspaces, activity) to their owning
domain modules. `AppDatabase` now carries only three methods:
- createHireRequest (multi-module composite: agents + approvalsMod)
- rollbackConfigRevision (multi-module composite: documents + entityCrud)
- queryAll (raw-SQL escape hatch, @internal)
The vault/env-binding infrastructure previously passed argument-by-
argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at
module construction, letting callers invoke `db.entityCrud.saveSecret({...})`
without carrying vault plumbing.
Structural types that narrowed AppDatabase to a bag of flat methods
(`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in
budget-enforcement / budget-incidents / task-quality-gates /
failure-escalation / task-quality-gates-sweep) are reshaped to the new
namespace. `connectorPolicyDepsFromDb` replaces the old pattern of
handing the whole AppDatabase to `validateConnectorModelSelection`.
A new ESLint rule in eslint.config.mjs blocks new top-level methods on
AppDatabase so the pass-through layer cannot silently grow back.
`AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost
the 270-line trailing pass-through block.
Before this commit `createDocumentWithLifecycle` enqueued ledger rows
inside the save transaction but consumed them from an in-memory
`pendingSideEffects` array straight after commit. A crash between the
commit and the drain stranded rows as `pending` forever; a crash mid
handler left them as `running`; and there was no retry budget.
The new `DocumentLifecycleProcessor` is the single drainer. It:
- claims work via `claimPendingBatch`, an atomic `UPDATE … RETURNING`
that flips rows from `pending` to `running` while incrementing
`attempts`, so two drainers cannot race onto the same row;
- dispatches each row through a handler registered by `hookType`,
parsing the frozen `payload_json` back into the call arguments;
- records `succeeded` / `failed` / `retryable` outcomes through the
ledger, so a transient failure stays on the queue until the attempt
cap expires, after which it is marked `failed` for operators;
- reclaims rows stuck in `running` past `staleRunningMs` back to
`pending` on every sweep tick plus at startup, so a crashed process
never leaves a row orphaned;
- runs a periodic sweep so every document — including those whose
immediate post-commit drain failed — is eventually advanced.
`createDocumentWithLifecycle` now only enqueues and then calls
`lifecycleProcessor.drainForDocument` for the immediate happy-path pass.
Handlers live in `document-lifecycle-handlers.ts` so the processor stays
free of service-graph knowledge. The processor is constructed in
`bootstrap-application`, started there, and stopped on `before-quit`.
Desktop IPC saves used to fire only automation and the reviewer wake,
while HTTP API saves fired automation, thread integration, checkpoint
engine, deliverable-review approval, and the reviewer wake. Two
divergent paths produced two different lifecycle states for the same
document, and a future fix to either branch would miss the other.
Introduce `document-lifecycle.ts` as the single owner of document
creation side effects: save, derive task binding from the originating
run, fan out to automation/thread/checkpoint, wake the reviewer, then
raise a deliverable-review approval when the document qualifies. The
HTTP route and the IPC handler both call the pipeline — the API
server's `onDocumentCreated` callback is replaced with a
`createDocument` hook that delegates to the pipeline.
Wire `checkpointEngine` through `bootstrap-application` →
`main-runtime` → `main-runtime-support` so the IPC handler bag carries
both thread integration and the checkpoint engine required by the
pipeline. The document-run-provenance integration test harness is
updated to pass a `createDocument` implementation backed by the same
pipeline the production path uses.
The per-connector "enabled/disabled model" settings lived under a
global key in the settings key-value table
(`disabled_models:${connectorId}`). No company dimension. Deleting a
company left the pool choices behind, and creating a new company
silently inherited them — the visible symptom was "onboarding's team-
model configuration survives company deletion and pollutes the next
company's CEO model list."
Root fix, not a cleanup hook: the storage becomes a real relational
table with `(company_id, connector_id)` primary key and an
`ON DELETE CASCADE` FK from `companies`. Company deletion wipes pool
config by construction; no cleanup step in `deleteCompany()` can
drift.
Schema (migration v42):
- CREATE TABLE connector_model_pool_settings (company_id, connector_id,
disabled_models_json, updated_at), PK (company_id, connector_id),
FK companies(id) ON DELETE CASCADE.
- Drop every `settings.disabled_models:*` key — these were the bug's
residue; there is no right company to attribute them to, so the
migration clears them. Users re-select on next onboarding.
API surgery — every reader/writer now takes companyId:
- db.getDisabledModels(companyId, connectorId)
- db.setDisabledModels(companyId, connectorId, models)
- db.hasModelPoolSettings(companyId, connectorId)
- connector-model-policy: getConnectorModelPolicy(deps, companyId,
connectorId); validateConnectorModelSelection(deps, companyId,
connectorId, model)
- model-dispatch: listEnabledModels(companyId, deps);
pickModelForAgent resolves companyId from agent.companyId
- orchestration heartbeat/task-runner/turn-context/run-dispatch pass
the agent's or run's companyId through to the policy deps
- IPC schema: connectorDisabledModelsSchema gains companyId; new
connectorModelPoolQuerySchema pairs companyId+connectorId for reads
Onboarding becomes commit-on-launch:
- useConnectorModelSelection no longer persists via IPC during
onboarding. Toggles mutate local state; it exposes `poolSelections`
to the wizard.
- bootstrapOnboardingSchema gains connectorPoolSelections. The
onboarding service saves the company first, then persists each
per-connector disabled list scoped to the new company id — CEO
model validation runs against the company's freshly-persisted pool,
not some stale global.
- IPC setConnectorDisabledModels now requires companyId; the only
consumer was onboarding, which no longer calls it at all.
- Main-side listConnectorModels returns the raw provider catalog
(no pool filtering), since that IPC is used from onboarding where
no company exists yet; pool filtering happens at dispatch time.
saveConnector IPC drops the per-company pool validation — connector
config is a global hint, company-scoped enforcement belongs at run
dispatch where the company is known.
Tests: 603/603 green. Typecheck clean. Integration tests rewritten
to set the pool via connectorPoolSelections on bootstrap input, not
a pre-bootstrap global setDisabledModels call.
Drop code that nothing consumed. Verified by full renderer+shared grep:
every removed channel had zero callers via window.agentCompany.* and
zero references from desktop-api types. No backend internal use either —
the database/API-server layers keep their methods, only the IPC exposure
goes.
Removed IPC handlers (26 total):
Thread CRUD surface (12) — writes happen inside the backend
createThread, listThreads, addThreadEvent,
setThreadWaitState, resolveThreadWaitState,
addParticipant, removeParticipant, listParticipants,
subscribeThread, unsubscribeThread,
listSubscriptions, listSubscribedThreads
(kept reads: getThread, listThreadEvents, listThreadWaitStates)
Half-implemented features (13)
saveSecret, listComments, getAgentMetrics, getApiPort,
listWakeupRequests, listAgentMessages, autoAssignTask,
listRunEvents, recoveryDismiss,
getHeartbeatPolicy, setHeartbeatPolicy,
getRuntimeState, setRuntimeState
Whole vertical slice: openConnectorAuthTerminal
IPC handler + channel + preload + DesktopApi type +
connector-auth-service function + useConnectorCallbacks
handleLoginConnector + OnboardingContext.onLoginConnector +
OnboardingWizard wiring. Pi-mono has no CLI auth flow;
the no-op that "returned false for legacy UI compatibility"
had no legacy UI left.
Orphaned modules (zero importers in repo):
src/main/runtime-env.ts
src/main/database-stale-recovery.ts
Dead schema files (only referenced by their own barrel re-export):
schemas-participants, schemas-subscriptions, schemas-heartbeat,
schemas-run-events, schemas-runtime-state, schemas-wakeup
Renderer leftovers:
.activity-row-enter CSS + its two unreferenced keyframes
5 orphan i18n keys (app.console, app.refresh, app.expand,
activity.timeline, auto.trigger) left over from earlier feature
deletions
Also update two unit tests that mocked openConnectorAuthTerminal in
the RegisterCoreHandlersDependencies pick.
The `registerCredentialIpcHandlers` and `registerOAuthIpcHandlers`
entry points existed in `ipc-credentials-handlers.ts` and
`ipc-oauth-handlers.ts` but were never called — `registerHandlers()`
in `main-runtime-support.ts` only wired `registerCoreHandlers` and
`registerDomainHandlers`. The result: every renderer call to
`credentials:list` / `credentials:save` / `credentials:remove` /
`credentials:test` / `oauth:start` / `oauth:cancel` /
`oauth:prompt-response` / `oauth:manual-code-response` crashed
with "No handler registered".
Wire both handler families against the PiRuntimeServices container
(credentialStore, credentialResolver, oauthLoginService) already
exposed via `serviceGraph.getPiRuntime()`. The refreshConnectorHealth
callback reuses the existing connectorHealthService so status
badges flip between `ready` and `auth_required` immediately after
a save/login.
typecheck 0 / lint 0-0 / build 0-0 / test:unit 508/508 preserved.
- Create ipc-budget-handlers.ts with 6 budget channel handlers (save/delete/list policies, list/resolve incidents, get status)
- Add budgetService to RegisterDomainHandlersDependencies interface
- Register budget handlers in ipc-domain-handlers.ts composition
- Add BudgetEnforcementService lazy slot in service-graph.ts with getOrCreateBudgetEnforcementService
- Pass budgetService to domain handler registration in main-runtime-support.ts
- Fix preload bridge to pass {companyId} object for listBudgetPolicies and getBudgetStatus
- D-06: checkAutoResolve called on saveBudgetPolicy to auto-resolve incidents on limit increase