The three third-party keys were hardcoded or read from the environment.
Operators now enter them in Settings > Integrations; they are stored
encrypted in the app-wide integration_keys table (migration 058).
An agent holding connector grants fails loudly when no Composio key is
saved. The goal-review test no longer depends on absolute dates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New oauth/kimi-code-flow.ts mirrors the official kimi-cli: form-encoded device_authorization + token polling at auth.kimi.com with the required X-Msh-* device headers, pure poll interpreter, 15-minute tokens refreshed via refresh_token grant
- Agent loop runs chat completions at https://api.kimi.com/coding/v1 with the Bearer access token; default model kimi-for-coding (kimi-for-coding-highspeed for Allegretto+ plans)
- Live model listing via /coding/v1/models; credential refresh wired into the turn credential resolver
- Migration 057 seeds the provider (disabled) into existing organizations
- Wire protocol probed live: device code issued, authorization_pending poll observed, 401 envelope on the coding API confirmed
The board draining to 280/280 done left the company idle under a
still-active goal, with 24 markdown files and zero code as its life's work.
Two structural gaps caused it: goals had no lifecycle (status was written
once as 'active' and never read again — nothing re-engages anyone when the
last task closes), and nothing anywhere distinguished plans from product,
so every "engineering" objective closed on documents.
The loop, closed:
- tasks.origin (migration 055) records provenance: chat_wake for mention
vehicles (backfilled from their runs), goal_review for checkpoints, null
for real work. Chatter and checkpoints can therefore never count as
progress.
- spawnGoalReviews (each worker tick): an active goal with zero open tasks
and real progress since the last review gets ONE review task for the goal
owner. The review demands evidence — list the workspace, read the code,
run the tests; documents are how work starts, not proof it happened — and
must end in exactly one of: complete_goal, or the next wave of tasks with
requires_filesystem engineering. A review closed with neither spawns
nothing: termination is structural, not a counter.
- complete_goal (CEO or goal owner) finally lets someone say a goal is
achieved, audited.
- CEO and department-head voices now state the doctrine: a goal that
promises software closes on code that builds and passes tests in the
workspace, and implementation is delegated with requires_filesystem: true.
On the live profile this arms one review: the active platform goal has 0
open tasks and 21 real completed ones, so the CEO gets the checkpoint on
next launch and the company starts building instead of stopping.
Of the 202 tasks stranded in the backlog on July 9, 200 were "Reply to ..."
wake vehicles: every agent @mention creates a task plus a queued run, the
reply goes out, and nothing ever closed the task — a task with a completed
run is never enqueued again, so each one became a permanent zombie row. On
top of that, every message in an active thread minted a fresh wake even when
the target already had one queued, multiplying runs that would each read the
same conversation.
The lifecycle contract, made explicit:
- The reply run IS the wake task's deliverable. Completing a chat_mention
run now closes its task (backlog/in_progress -> done, via the audited
status path). Tasks an agent moved to review are left alone.
- One pending wake per (agent, thread): wakeFromMention returns the queued
run instead of minting a duplicate — that run reads the thread's newest
messages when it executes anyway.
- Migration 054 heals existing rows: wake tasks whose runs are all spent
close as done; in-flight wakes keep working. This also stops Fix 1's
retry-budget healing from resurrecting 31 stale replies on next launch.
The chat-governance loop test now completes each pending wake run between
posts, matching how the exchange actually interleaves in production.
Two operator asks, one review gate:
Avatar portraits (migration 052): the avatar editor gains "Upload
photo" - the renderer downscales the chosen picture to a ~96px cover
crop and stores it as a small data URL (PNG keeps transparency, the
rest compresses to JPEG), so the snapshot stays light and the CSP needs
nothing new. The portrait wins over emoji and initials everywhere the
shared Avatar renders (chat, org chart, office card, sidebar), and
"Remove photo" falls back cleanly. The main process validates scheme
and size (base64 image data URL, <=300k chars) so a rogue caller cannot
re-bloat the snapshot. e2e uploads a real PNG through the profile
dialog and sees the portrait land in the sidebar.
Global pause (migration 053): when agents start talking in circles,
one topbar toggle - visible on every page - pulls the handbrake.
app_state.autorun_paused survives restarts; the queue worker checks it
each tick and neither enqueues nor claims while set (the run already in
flight finishes). Queued work simply waits and the paused state renders
as an amber "resume" pill. A unit test drives the real worker: paused
leaves a queued run untouched through many ticks, resuming claims it
within moments; e2e flips the toggle in the live shell.
The whole app turned to molasses because snapshot.load() shipped the
entire skill_runtime_snapshots table - unbounded, compiled_prompt
included - over IPC on every refresh. A real profile carried 142 rows
totaling 144MB (1.1MB each from the pre-cap CEO compile-all era), so
every coalesced refresh serialized ~144MB of JSON. The forced-humanizer
rework made it worse by adding a row per run for every agent.
- Drop skillRuntimeSnapshots, skillRevisions, skillTests, and
skillBindings from the renderer snapshot: no renderer surface reads
them; skill internals are main-process state. Only the lightweight
skillDefinitions catalogue still ships.
- Project provider_requests without raw_response_json (4.1MB across the
newest 100 rows; RunsPage only reads responseText)
- Migration 051 purges the bug-era compiled prompts from existing DBs
- resolveRuntimeSnapshot is now idempotent per run: a run re-queued by
startup recovery reuses its frozen snapshot instead of dying on the
uniqueness guard and burning real retry budget
- Regression test pins the contract: a 2MB database row must not add
bytes to the snapshot (payload asserted under 400KB)
Researched how OKR-run companies cascade (company objective -> C-suite
officers per function -> department objectives -> individual key
results) and encoded that shape into how the CEO builds the org.
- hire_agent gains seniority (director/lead/staff; c_suite stays
reserved for the CEO's privileges) and a business-card title, so the
CEO hires a CTO/CFO/CMO as directors instead of a flat pool of ICs
- CEO persona + goal kickoff now direct the OKR cascade: hire 2-4
officers, hand each a department objective, let them build teams;
department heads get their own doctrine (hire leads/staff, cascade
key results as tasks, report up)
- Migration 050 adds Finance and Sales to every org's canonical
departments (backfilled for existing DBs) so a real C-suite has
somewhere to hire
- Org chart viewport is a fixed-height scroll region: zooming rescales
the diagram inside it instead of resizing the card
- Unit test drives a live three-level chain: CEO -> CTO (director) ->
Engineering Manager (lead) -> staff, with rank/title persisted
- Migration 049: avatar_emoji/avatar_color on agents and employees
- updateAgent accepts a new name (mention-name validated) and avatar;
new updateEmployeeProfile renames the operator and sets their avatar
- Avatars are emoji-on-color tiles (renderer CSP blocks remote images);
an AvatarDirectoryContext built from the snapshot lets the shared
Avatar component honor custom looks at every call site - chat rows,
conversation list, org chart, office card, sidebar - without
threading props through pages
- Team edit dialog gains Name + avatar editor (CEO included); the
sidebar foot opens an Edit-profile dialog for the operator
- e2e: rename an agent to a new mention handle with an emoji avatar,
edit the operator profile, both verified in the live shell
Root cause of the recurring 401: a revoked key was hardcoded in
startup-recovery.ts and onboarding-service.ts and reseeded into
composio_settings on every launch, and the interim fix shipped cred.txt
as a visible file under resources/.
- electron.vite.config.ts reads the gitignored cred.txt at build time
and injects the key into the main-process bundle as
__COMPOSIO_API_KEY__; no credential file is packaged and the key is
not user-visible or configurable. The renderer bundle never sees it.
- Delete both hardcoded-key seeders; migration 046 purges stored
api_key and never-read toolkits rows while keeping entity_id and
auth_config state.
- Stop shipping composio_settings in the renderer snapshot.
The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
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.
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.
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.
Two sites in upsertFilesystemArtifact leaked null workspace_ids
into the work_products table and produced the orphan rows the read
path now has to heal:
1. Create path unconditionally forwarded ctx.workspaceId, which was
null whenever a run was created without a workspace binding
(historically cognitive runs, or runs that lost their link through
the observationPath regression fixed in 1f2f977).
2. Update path passed {workspaceId: ctx.workspaceId} as part of the
patch. updateWorkProduct treats any key present in patch as "write
this value", so a run touching a previously-well-linked row with
ctx.workspaceId=null would clobber the existing workspace_id back
to null — actively destroying data.
Resolve both at the projector layer:
- Add workspacesMod.findWorkspaceIdByLocalPath(companyId, localPath)
as a reverse-lookup. workspacePath is the argument the projector
is already walking; it came from some workspace row's local_path,
so finding that row by path is deterministic.
- Compute resolvedWorkspaceId = ctx.workspaceId ?? reverseLookup(...)
once, and use it for both the identity lookup
(findWorkProductByExternalId) and the subsequent write.
- On the update path, only include workspaceId in the patch when
resolvedWorkspaceId is non-null. Null now means "no info — leave
the existing column alone" instead of "clobber to null".
Together with the read-path heal in cfb8ba5 this closes the loop:
new files never enter the table with a null workspace_id if we can
possibly resolve it, old files keep their links, and truly unlinkable
rows still fall back to the observation workspace on open.
Typecheck and build:verify clean.
Work-product rows written before the artifact projector tracked
workspace_id end-to-end can lose the link through all three joins:
workspace_id is NULL, task_id is NULL, and the row's run exists but
its runs.workspace_id is also NULL. Those orphans were readable in
the DocumentCenter (the listing doesn't need a workspace root) but
could not be opened — resolveWorkProductTarget returned null and the
toast now reports workspace_unresolved.
Add a fourth resolution tier in resolveWorkProductTarget: if the
three-step chain yields nothing, look up the company's observation
workspace (primary first, then oldest) and check whether the
external_id resolves to a real file inside that root. If it does,
return that absolute path and the row opens. If the file isn't
there, the error falls through with an extended detail line naming
the observation root that was tried, so the operator sees exactly
why the heal also failed.
This is deterministic — exactly one fallback workspace — and
side-effect-free on the read path: the row itself is left alone,
no stealth write-back. A follow-up commit will stop the projector
from writing NULL workspace_id in the first place.
Typecheck and build:verify clean.
The previous handler folded every null target into one error code
("WORK_PRODUCT_NOT_FOUND: Work product not found or its workspace
is unresolved"). When a user couldn't open a file we had no way to
tell whether the row was archived, the provider/external_id was
empty, the workspace had no local_path, or the external_id escaped
the root.
Widen resolveWorkProductTarget's return from (target | null) to
(target | error) where error carries one of five reasons:
not_found row missing for id+company
archived status == "archived" tombstone
no_target provider != filesystem and no url
workspace_unresolved all three workspace_id / task_id / run_id
lookups returned no local_path
path_escape external_id is absolute or steps outside root
Each reason includes the concrete detail (ids checked, paths tried)
so the toast on the renderer side tells the user exactly which slot
is null without having to open the DB.
The IPC handler maps each reason to its own failure code and passes
the detail through, so WorkProductRow's toast now reads e.g.
"workspace_id=null task_id=null run_id=null — none led to a
workspace with a local_path" instead of a generic unresolved.
Typecheck and build:verify clean.