Commit Graph
95 Commits
Author SHA1 Message Date
ZacharyZhang-NY 1f89ea7089 Stop shipping megabytes of skill prompts in every snapshot refresh
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)
2026-07-08 23:20:56 -04:00
ZacharyZhang-NY e55b8b85b0 Make every identity editable: agent and operator names plus avatars
- 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
2026-07-08 21:33:23 -04:00
ZacharyZhang-NY 86aa6fdb29 Ship skills built-in: bundle the catalogue, assign per hire, force humanizer
Skills no longer come from the user's ~/.agents library - most users have
none, and the CEO branch that compiled the entire catalogue into its
prompt overflowed the model context (1.1MB) and failed every CEO run.

- Bundle "built-in skills/" with the app (extraResources) and stage it
  at startup into <profile>/builtin-skills as one dir-per-skill copy -
  both bundle shapes (SKILL.md dirs and flat category .md files)
  normalize into the same importable layout
- Import the staged catalogue per org at startup and inside onboarding,
  so the CEO's first run already carries it
- Forced house style: resolveRuntimeSnapshot unions the humanizer skill
  into every agent's runtime (CEO included) as a resolution rule - no
  binding row to forget, delete, or bypass; revoke_skill refuses it
- CEO staffs hires with skills: hire_agent accepts a skills array,
  grant_skill/revoke_skill adjust later, list_skills now shows
  descriptions from skill frontmatter
- Kill the c_suite compile-all branch; everyone resolves bindings-only,
  capped at 200k chars with omissions named in the prompt
- Remove the Skills settings page and its import/sync/test IPC surface;
  drop ~/.agents discovery and the AGENTCOMPANY_NEW_SKILLS_ROOT seam in
  favor of AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR
- Tests: staging/import/forced-resolution/cap units, real-bundle CI
  check, e2e asserts the tab is gone and the catalogue self-imports
2026-07-08 21:18:50 -04:00
ZacharyZhang-NY 349547a0de Resume parked work when a provider's monthly limit is raised
The heartbeat scheduler enqueued runs even while a provider's monthly
usage window was full, so every task burned its 3-attempt retry budget
on instant quota failures within seconds and stayed dead forever -
raising the limit (or restarting) never revived anything.

- Park tasks (skip enqueue) while their provider is at its
  monthly_request_limit instead of creating runs doomed to fail
- Exclude quota-blocked failures from the 3-attempt retry budget,
  retroactively reviving tasks abandoned by past filled windows
- Share the gate message as PROVIDER_MONTHLY_LIMIT_MESSAGE and log
  quota-block transitions once per state change for diagnosability
- Ignore landing/ (self-contained Next.js sub-project) and
  built-in skills/ (prompt content) in eslint so verify:ci lints only
  application code
2026-07-08 20:51:24 -04:00
ZacharyZhang-NY 3e4c7c81a2 Localize the renderer UI into Chinese behind the language setting 2026-07-07 13:22:19 -04:00
ZacharyZhang-NY d404e1e72a Pin the UI locale to the language setting and add theme + language selectors with a dark palette 2026-07-07 12:28:06 -04:00
ZacharyZhang-NY 38bd3588e7 Connected connectors can be deleted and reconnected; mirror sweeps stale rows and ops serialize per org 2026-07-07 12:09:31 -04:00
ZacharyZhang-NY defe8f12b1 Make the CEO the grant authority: implicit all-access, agent-tool grants, no manual assignment UI 2026-07-07 11:24:28 -04:00
ZacharyZhang-NY 00e05054ca Mirror the Composio project's auth configs as the connector catalog with logo.dev icons 2026-07-07 11:06:07 -04:00
ZacharyZhang-NY d19573187c Fix connector Connect: use the link flow and surface failures
Root cause found by probing the live API: Composio v3 rejects
POST /connected_accounts for Composio-managed OAuth auth configs with a
400 pointing at POST /connected_accounts/link, and the renderer threw
the failure away inside a .then, so clicking Connect did nothing and no
browser opened.

- createConnectedAccount now calls POST /connected_accounts/link with
  auth_config_id and user_id and reads connected_account_id and
  redirect_url; the local mirror records INITIALIZING until the poll
  adopts Composio's real status.
- Connect failures (and a blocked external-URL open) now show as a
  single line on the page instead of vanishing.
2026-07-07 01:53:22 -04:00
ZacharyZhang-NY a2de132083 Embed the Composio key in the app bundle instead of shipping cred.txt
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.
2026-07-07 01:40:19 -04:00
ZacharyZhang-NY 2d43950123 Read skills from the global and workspace .agents/skills directories
- Discover skill directories under ~/.agents/skills and each
  workspace''s .agents/skills; a directory qualifies with a full
  skill.manifest.json or just a SKILL.md, whose manifest is derived
  (frontmatter name or the directory name). Paths resolve per platform
  on Windows, macOS, and Linux.
- Sync at startup for every organization and again when the Skills
  page opens; skills the org already carries are skipped, and one
  broken directory cannot block the rest.
- AGENTCOMPANY_NEW_SKILLS_ROOT points tests at a temporary library
  (overriding USERPROFILE crashes Electron on Windows).
2026-07-07 01:21:33 -04:00
ZacharyZhang-NY 256e0680c6 Ship Composio as a built-in integration and trim Settings
- Resolve the API key from cred.txt at the project root in development
  and the packaged resources directory in production; the file is the
  source of truth, so a rotated key replaces the stored one. Fixes the
  401 packaged builds hit when a stale database key won.
- Remove the user-facing Composio key editor; the key ships with the
  product and is not configurable.
- Remove the Migration console from Settings.
- Remove the Connectors and Home refresh buttons; connector statuses
  sync when the page opens.
2026-07-07 01:20:21 -04:00
ZacharyZhang-NY 07d0b4587d Promote AgentCompany-New to the repository root and retire the old app
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.
2026-07-07 00:04:13 -04:00
ZacharyZhang-NY b92cf62c4c fix(data): canonicalize operation json fields 2026-05-15 02:36:19 -04:00
ZacharyZhang-NY 8b0b06f765 fix(security): validate bedrock endpoint URLs 2026-05-15 00:52:18 -04:00
ZacharyZhang-NY 84b92c859e fix(security): restrict local connector endpoints 2026-05-15 00:43:45 -04:00
ZacharyZhang-NY 66e24b1060 fix(security): scope managed path opening 2026-05-15 00:26:20 -04:00
ZacharyZhang-NY a532120649 feat(connectors): add Ollama and LM Studio as a local auth kind
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.
2026-05-14 14:55:38 -04:00
ZacharyZhang-NY 07d9c00fab feat(connectors): upgrade Fire Pass router to Kimi K2.6 Turbo
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.
2026-05-14 14:39:32 -04:00
ZacharyZhang-NY 31aac7c0de fix(messages): normalize escaped markdown newlines 2026-04-24 10:35:11 -04:00
ZacharyZhang-NY 227fd7faf2 refactor(ipc): validate write payloads in preload 2026-04-24 01:31:29 -04:00
ZacharyZhang-NY 67ccfc1160 fix: enforce work product taxonomy 2026-04-23 22:44:41 -04:00
ZacharyZhang-NY 9e712f2746 refactor(ipc): generate preload forwarders from registry 2026-04-22 16:49:02 -04:00
ZacharyZhang-NY eefb515aa2 fix(runtime): classify run usage status 2026-04-22 15:41:19 -04:00
ZacharyZhang-NY 19f8494d09 fix(db): make filesystem lock intent explicit 2026-04-22 13:51:41 -04:00
ZacharyZhang-NY 95c4b0ae48 refactor(design): migrate hardcoded colors to kami tokens
Purge remaining hardcoded color values from the component layer and main
process so every surface flows through the kami token system.

src/main/desktop-shell-window.ts
  - BrowserWindow.backgroundColor no longer paints pure white before the
    renderer mounts. It now picks kami parchment (#F5F4ED) or deep-dark
    (#141413) based on nativeTheme.shouldUseDarkColors so the startup
    flash respects the user's OS theme preference.

src/shared/constants.ts
  - DEPARTMENT_COLORS collapses 12 unrelated hex brand colors (violet,
    red, green, cyan, pink, ...) into six kami tokens: var(--primary),
    var(--kami-ink), var(--kami-dark-warm), var(--kami-charcoal),
    var(--kami-olive), var(--kami-stone). Departments are distinguished
    by warm-neutral ramp + ink-blue, never by second chromatic hue.

src/renderer/components/communication/{MessageItem,SearchResults}.tsx
  - Replace the string-concat tint pattern `${color}18` with
    color-mix(in oklab, ${color}, transparent 91%). The old pattern
    produced invalid CSS (e.g. `var(--primary)18`) whenever `color`
    was a CSS custom property, silently falling back to no background.
    color-mix works for both hex and var() inputs, eliminating the
    latent bug at the source.

src/renderer/components/ui/ActionButton.tsx
  - Replace hand-rolled rgba(74,93,122,0.35) and rgba(220,53,69,0.3)
    drop shadows with kami ring shadows routed through color-mix over
    var(--primary) and var(--destructive).

src/renderer/views/CommandPaletteView.tsx
  - Replace rgba(15,23,42,0.08) cold-slate shadow with a kami whisper
    shadow driven by color-mix over var(--foreground).

src/renderer/components/skills/SkillAgentToggles.tsx
src/renderer/components/execution/ModelManagementShared.tsx
  - Toggle thumbs switch from bg-white to bg-background so they
    inherit parchment in light mode and deep-dark in dark mode,
    removing the last pure-white surfaces from the renderer.

Third-party brand hex values in src/renderer/components/social/
social-config.ts are retained: they represent external brand identity
(Twitter blue, YouTube red, ...), not kami chrome, and collapsing them
to one hue would destroy recognition.

Typecheck clean. No business-logic signatures changed.
2026-04-21 13:38:06 -04:00
ZacharyZhang-NY 97033dfabd update 2026-04-21 12:44:33 -04:00
ZacharyZhang-NY 7829acc7f0 Add tasks.requires_filesystem column (v52) — per-task filesystem lock policy
Before v52, resolveWorkspaceWithLockMode walked a four-step fallback
chain (task → agent → project → any company workspace) and marked
every resolved row exclusive. In a multi-agent company every heartbeat
collapsed onto the same company workspace row, every run claimed the
same exclusive lock, and only one agent ever executed at a time.

Root cause is a missing column: per-task 'does this task touch the
filesystem?' had no place to live, so the orchestrator defaulted to
yes on everything. v52 gives that question its own column.

Backfill is conservative:
  - in-flight tasks (in_progress + active_run_id) → 1 (preserve
    the lease the dispatcher already holds)
  - todo/backlog tasks whose assignee is an engineering-flavoured
    role (CTO/engineer/developer/devops/sre or department =
    engineering) → 1
  - everything else → 0

TaskRecord now exposes requiresFilesystem on both entityCrud.getTask
and the snapshot read path.
2026-04-20 15:25:36 -04:00
ZacharyZhang-NY e5e293085b Revert Fire Pass model id to the router path
The previous pass "aligned" model-resolver.ts to
accounts/fireworks/models/kimi-k2.5-turbo — the per-token model path —
to match the connector catalog's default. That was the wrong direction:
Fire Pass subscription traffic routes exclusively through
accounts/fireworks/routers/kimi-k2p5-turbo (see docs.fireworks.ai/firepass),
and the entitlement does not attach to the per-token id. Existing agent
rows already pointed at the router id, so validateConnectorModelSelection
rejected every dispatch with "does not allow model
'accounts/fireworks/routers/kimi-k2p5-turbo'" and the CEO stopped running
entirely.

Put the router id back in model-resolver.ts (the enabled-pool source of
truth), and bring the two catalog defaults in line so a freshly-seeded
Fire Pass connector also ends up on the router id. The per-token
`fireworks` connector keeps the `models/...` default since that path
bills per token and genuinely targets the per-model endpoint.
2026-04-19 22:14:42 -04:00
ZacharyZhang-NY 2e60ecc0e0 Distinguish top-level review wakes from subtask completion
Top-level tasks submitted to in_review fired the same
subtask_completed wake as the subtree leaves, so the reviewer's
prompt framed the work as "a subtask you delegated". Add a dedicated
task_review_requested wake reason with a reviewer-specific title and
step list, and route the parent-less branch of the task PATCH handler
through it.
2026-04-19 19:26:05 -04:00
ZacharyZhang-NY d9b35d73ab Pin work_products identity to (company, workspace, path)
The natural key used to be (companyId, provider, externalId), so two
workspaces with the same workspace-relative path collided onto one
row. Provenance was also frozen at the first write: `created_by_run_id`
never refreshed, so a run that edited a pre-existing file never
qualified as a deliverable and the DocumentCenter "open" link could
still jump into the first workspace after a repo move.

Migration v45 adds `workspace_id` + `last_touched_by_run_id`, backfills
`workspace_id` via the task/run join, seeds `last_touched_by_run_id =
created_by_run_id`, and installs a partial unique index on
`(company_id, workspace_id, provider, external_id)` scoped to rows
that have a workspace. The artifact projector now records
`workspace_id` on insert and refreshes `last_touched_by_run_id` on
every upsert. The deliverable signal query reads either provenance
column so a run that only edited a pre-existing file still counts.
`resolveWorkProductTarget` prefers the stored `workspace_id` and falls
back to the task/run join for legacy NULL rows.
2026-04-19 15:43:48 -04:00
ZacharyZhang-NY ff9a52324e Route work-product clicks through a resolver IPC
Filesystem work products store `external_id` as a workspace-relative
path so artifacts survive a workspace-root move, but the renderer was
handing that relative fragment straight to `shell.openPath`, which
needs an absolute managed path. Clicks flickered into a failed open.

Add a dedicated `openWorkProduct({ companyId, id })` IPC backed by
`resolveWorkProductTarget` on the DB layer. The resolver joins the
work-product row back to its originating task → workspace (or run →
workspace for taskless runs), composes an absolute path, and rejects
any relative fragment that would escape the workspace root. The
renderer now passes the work product's id — path resolution stays on
the main process where the managed-path policy already lives.
2026-04-19 15:05:21 -04:00
ZacharyZhang-NY 22ba7a9004 feat(documents): surface filesystem work-products in Document Center
The Document Center only read the `documents` table, which is prose
content POSTed via /api/documents. Everything the agents wrote via
`bash` / `write` / `edit` — the bulk of their output on a real
project — landed in `work_products` and never appeared on the page.
Operators saw "0 items" after a full night's run.

Lift work_products into the same UI:

  - `listWorkProductsForCompany` returns a newest-first slice with a
    500-row cap so the snapshot stays bounded.
  - `ProfileSnapshot.workProducts` flows through `useCompanyData` as
    `companyWorkProducts`, filtered to the current company.
  - `DocumentCenter` renders two labelled sections in order — prose
    documents (editable) on top, files & deliverables below. Each
    work-product row opens the underlying URL or filesystem path
    when clickable, so the agent's bash-generated markdown, PRs,
    and artifacts are one click away.

`WorkProductRecord` lifts into `@shared/types/records-extended` so
the renderer, snapshot, and database shells all reference the same
shape. The store-test fixture learns about the new field.
2026-04-19 13:24:50 -04:00
ZacharyZhang-NY 0a40c55b3c fix(thread-panel): wire the scoped reply composer to real participants
The thread-panel composer was pinned to `channel="direct"` with no
`directParticipantIds` and no `replyTo`. Clicking reply in a human
review thread sent a message with no recipient and the send button
stayed disabled — a silent dead-end for the entire Co-Founder
review UX.

The fix exposes the thread's participant list to the renderer:

  - `listThreadParticipantsSchema` and the matching IPC channel
    (`agent-company:list-thread-participants`) return the rows from
    `conversation_participants` for a given thread.
  - `threadIntegration.onTaskCreated` / `onApprovalCreated` already
    enrol the owning agent and the virtual user on thread creation,
    so the list is populated without any backfill.
  - `ThreadPanel` loads participants alongside events and forwards
    the agent ids to `MessageComposer.directParticipantIds`. The
    composer's existing `directRecipientId` memo picks the first
    non-board agent, so replies now target the correct agent — or
    surface the "no recipient" notice when the thread has none.

The Board/user member is deliberately excluded from the list the
composer consumes: the composer wants an agent to route to. All
other wiring (wait-state precision, threadId) stays untouched.
2026-04-19 01:19:23 -04:00
ZacharyZhang-NY f041336721 refactor: nuke dead exports, dead files, dead DB column, dead zero-value wrappers
Full-codebase ts-prune + manual audit sweep. Every change below is a pure
deletion (or a 2-line re-wire where a dead function was supposed to be
alive). No scope additions.

Dead wrappers / phantom features
  - shouldAutoApproveApproval: zero-value wrapper over shouldAutoApprove.
    Four call sites now import shouldAutoApprove from execution-policy
    directly.
  - evaluateRotation(session, connectorId, thresholdsOverride): the
    connectorId arg was never read (underscored), and thresholdsOverride
    was only exercised by the test suite — no production caller. Signature
    collapses to evaluateRotation(session). DEFAULT_THRESHOLDS /
    RotationThresholds exports and their tests go with it.
  - installLogRedactor: defensive console wrapper that was never wired
    from worker-entry. buildRedactor handles the known threat (env-value
    leaks); the pattern-based redactTokens stays for tests and future
    composition.

Dead files
  - resume-interrupted-tasks.ts: helper function never imported.
  - service-graph-orchestration-deps.ts: three factory closures
    superseded by the inline closures in create-orchestration-service.

Dead exports
  - listConnectorsByAuthKind (shared/connector-catalog)
  - getLocalePromptInstruction + whole localization.ts file
  - ImportantDocType
  - main-side ConnectorAuthKind (duplicated the shared one; not referenced)
  - CheckpointEngineService, RunHousekeepingService types
  - WorkspaceResolution type
  - cmdOrCtrlSymbol
  - AgentEventPush, CredentialPayload type aliases
  - parseApprovalPayloadJson, parseEnvValue, ProviderCard
  - getSOP — buildSOPSection reads the SOPS map directly; the individual
    getter had no callers. Deduped to a flat array.
  - renderWakeReason in wake-reasons/en.ts: byte-for-byte duplicate of the
    one in wake-reasons/index.ts.

Forgotten wiring (not cleanup, but adjacent)
  - registerAgentStandupRoute: the GET /api/agents/me/standup endpoint is
    advertised in every system prompt and wake-reason doc, but the route
    handler was never registered in api-server/index.ts. Agents have been
    hitting 404 since Pi Mono. Registered.

Dead DB column
  - v44 migration drops runs.context_snapshot_json. Column was added by
    v16-v25, never written or read, and v43's deliberate preservation
    was an interim measure. Same idempotent table-rebuild pattern as
    v41/v43.

Test tree
  - Phantom-feature threshold override tests removed (3). The rest of
    the rotation suite re-expressed against the real maxRuns=20 floor.

495/495 unit tests pass. typecheck + build clean.
2026-04-18 23:37:37 -04:00
ZacharyZhang-NY e0e43d6127 refactor(session): nuke dead sessionParams / sessionDisplayId pipeline (v43)
These columns and the messages that plumbed them existed for the CLI-
adapter era — Gemini's resume id and provider-specific session params
rode the run-finished envelope back onto the session row so the next
turn could resume. Pi Mono dispatches every provider through pi-ai and
emits neither value. The worker has been posting literal null on every
finish, finalizeRun has been writing null into task_sessions, and
rotateSession has been carrying null forward.

Remove the wire, the writer, the carry-forward, and the columns:

  - v43 migration rebuilds task_sessions without session_params_json /
    session_display_id, and runs without session_display_id. Follows
    the idempotent "create new, copy, swap, reindex" pattern used by
    v41. context_snapshot_json is preserved (also unreferenced today
    but outside this cleanup's scope).
  - RunJob / EnqueueRunPayload / ChildMessage / worker-types zod
    schema no longer carry the fields. finishRun() / createNewSession()
    signatures tighten in lockstep.
  - rotateSession starts the new session with fresh counters and no
    carry-forward.
  - turn-context resolveSession returns just the session id.
  - TaskSessionRecord / RunRecord lose the dead fields.

498/498 unit tests pass. typecheck + build clean.
2026-04-18 23:22:17 -04:00
ZacharyZhang-NY 88a1f8b334 refactor(connectors): drop dead CLI command field after pi-mono migration
The CLI-adapter era `connector.command` field became vestigial once every
provider dispatch went through pi-ai. The schema still required a non-empty
command, so onboarding saveConnector silently rejected every Pi Mono row
(which carries an empty default). Remove the field from the payload chain
(renderer → IPC schema → DB CRUD → run dispatch → worker job), narrow
RunJob.taskId to match EnqueueRunPayload, and drop the dead helper plus
its test. Schema column stays NOT NULL for DB compatibility; seedConnectors
writes a literal empty string so no migration is needed.
2026-04-18 23:09:59 -04:00
ZacharyZhang-NY d75bfd5851 refactor(model-pool): scope disabled-models by company; cascade on delete (v42)
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.
2026-04-18 22:14:09 -04:00
ZacharyZhang-NY 2ace9183b0 refactor(session): drop dead task-scoped session columns (v41)
Sessions have been agent-scoped since v34, but the schema kept four
dead task-scoped columns that every session code path had to pretend
were meaningful:

  - tasks.active_session_id     (mirrored agents.session_id)
  - tasks.session_json          (superseded by task_sessions.session_params_json)
  - tasks.session_display_id    (superseded by task_sessions.session_display_id)
  - task_sessions.task_id       (always empty-string on new rows)

v41 drops all four via SQLite table rebuild — the only safe path when
multiple columns need to go in one step and indexes / FK directions
need to be preserved. `PRAGMA foreign_keys = OFF` spans the swap
window so cascade-delete does not fire mid-rebuild. The migration is
idempotent and also backfills the optional task columns
(task_type / requires_user_review / retry_count / last_failure_at /
started_at / completed_at / cancelled_at / checkout_run_id /
execution_locked_at) before rebuilding so it works whether or not
`ensureOptionalColumns` has run yet on this install.

Along with the schema change, every code path that read or wrote
those columns has been rewired to the agent-scoped equivalents:

- task-sessions-crud.ts: deleted updateTaskSession,
  getTaskSession, getActiveTaskSession, updateTasksActiveSessionForAgent;
  added updateSessionParams(sessionId, ...) for the worker's
  finalizer. ensureAgentSession and createNewSession no longer touch
  task_id.
- database-base.ts: facade drops the now-unused pass-throughs.
- snapshot.ts / task-queries.ts / tasks-status.ts / entity-crud.ts:
  SELECT projections no longer pull session_json / session_display_id
  / active_session_id from tasks.
- tasks-crud.ts (TaskRow) and shared/types/records-core.ts
  (TaskRecord): sessionJson / sessionDisplayId fields removed.
- TaskSessionRecord: taskId field removed.
- turn-context.ts::resolveSession: no more
  updateTasksActiveSessionForAgent mirror; sessionParams read directly
  from the current session via getSessionById.
- run-finished-helpers.ts::finalizeRun: stamps sessionParams via
  updateSessionParams on the agent's current session, resolved through
  agents.session_id.
- session-management-service.ts: rotateSession stops calling
  updateTasksActiveSessionForAgent.

Test suite updated to use the new session-id API. Existing tests used
`getActiveTaskSession(taskId)` and `createNewSession({ taskId, ...})`
as probes; they now use `getSessionById(sessionId)` and the taskId-less
createNewSession input.

Tests: 603/603 green. Typecheck clean. Migration tested on fresh DB
(runs v1→v41 cleanly) and idempotent re-apply (detects absent dead
columns, skips).
2026-04-18 21:54:31 -04:00
ZacharyZhang-NY 6abc1a823f refactor(inbox): derive from snapshot; delete getInbox IPC chain
Sibling refactor to the sidebar-badges cleanup. The inbox aggregates
five item categories (pending approvals, failed runs, budget warnings,
blocked/review tasks, unread company/incident/urgent messages) — every
row is already in `ProfileSnapshot`. The server-side `getInbox`
aggregation (180 lines of SQL + projection) was a redundant second
path.

New: `src/shared/inbox-items.ts::deriveInboxItems(snapshot, companyId)`
produces the same InboxItem[] shape with the same severity ordering
and 24h failed-run window. Pure function, no IPC, no state.

Deleted:
- database/company-crud.ts::getInbox (180-line aggregation)
- database-base.ts::getInbox pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- state/slices/company.ts: `inboxItems` field, `setInboxItems` action
- useDataLoader.ts: both getInbox call sites (initial snapshot load
  and section-change effect)
- useCompanyController.ts: inbox reset on company switch

OperationsSections now computes `inboxItems` via `useMemo` off the
current snapshot + company id. The snapshot is already company-scoped
at the main/database/snapshot.ts layer when `currentCompanyId` is
non-null; the derivation re-filters defensively so the function works
correctly even for unscoped snapshots.

Tests: 603/603 green. No new tests added — the derivation is a pure
function whose shape is exercised end-to-end by the existing Inbox
component integration.
2026-04-18 21:42:58 -04:00
ZacharyZhang-NY 487f2456ff refactor(badges): derive from snapshot; delete getSidebarBadges IPC chain
Badges were fetched via a dedicated IPC that aggregated counts on the
server, while the same data lived in ProfileSnapshot on the client.
Two paths to the same answer drift whenever one side mutates between
refreshes — the visible symptom being "badge = 1 unread" with an empty
inbox after a message gets marked read in another screen.

Delete the whole server path. Sidebar badges are now derived on the
client from the snapshot via `deriveSidebarBadges(snapshot, companyId)`
in `src/shared/sidebar-badges.ts`. One source of truth; drift is
physically impossible.

Deletions:
- database/company-settings.ts::getSidebarBadges (120-line SQL aggregation)
- database-base.ts::getSidebarBadges pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- shared/contracts/schemas-badges.ts::sidebarBadgesSchema Zod schema
- renderer/state/slices/company.ts: `badges` field, `setBadges`,
  `refreshBadges`
- useDataLoader: both getSidebarBadges call sites
- useCompanyController: badge reset on company switch
- useDesktopEvents: refreshBadges call on granular patches
- tests/unit/store.test.ts: refreshBadges test (deleted code)

The snapshot's agentMessages already carries board-reader read state
via boardReadReceiptMap (`snapshot.ts:39-47` has always LEFT JOIN'd
agent_message_reads with agent_id = BOARD_READER_ID). So derivation
needs no schema change — the infrastructure was in place, just unused.

fix(communication/MessageList): render orphaned-reply messages at the
top level

The thread-flattening used `filteredMessages.filter(m => !m.parentMessageId)`
to pick top-level messages. When an agent replies to a company/incident
post with a DM (the CEO prompt explicitly instructs this in
wake-reasons/en.ts:119), the DM lands in the direct channel with
parentMessageId pointing to a message that lives in another channel.
That parent is not in `filteredMessages` for the direct channel, so the
reply had no parent to nest under AND was excluded from top-level. The
channel rendered as a blank pane even though exactly one message
matched — the "clicked Dr. X ↔ Dr. Y, got empty" symptom.

Fix: a message is top-level when it either has no parent OR its parent
is outside the current filtered view. Replies with in-view parents
still nest normally.
2026-04-18 21:38:34 -04:00
ZacharyZhang-NY ff5e22092a refactor(session): agent-scope rotation + accumulation; auto-register file artifacts
Sessions are already agent-scoped post-v34, but the code kept routing
rotation / accumulation / handoff through the task indirection. Heartbeat
runs (no task) silently skipped all three, letting taskless transcripts
grow past the model's context window — the 7M-token explosion.

- DB adds getSessionById(sessionId) — direct lookup bypassing
  tasks.active_session_id
- session-management-service: generateHandoff(session) is now agent-scoped,
  walking recent runs across tasks + heartbeats; getHandoffForAgent(agentId)
  is the canonical lookup, getHandoffContext(taskId) is a thin shim for
  callers that still hold a task id
- turn-context.ts::resolveSession drops the `if (!task)` early return:
  rotation evaluation runs on the agent session unconditionally
- run-finished-helpers.ts::accumulateAndRotateSession drops the `!task`
  gate: tokens accumulate and rotation fires for every run, keyed on the
  agent session, not the task
- Rotation evaluator contract: evaluatePreRunSessionRotation(agentId,
  connectorId) → { rotated, handoff, sessionId }; old (taskId, connectorId)
  signature removed

IPC contract tightening: ipc-support::emitEvent becomes a best-effort
sink. "Render frame was disposed" during renderer reload is a lifecycle
signal, not a logic error; silently drop it instead of letting it abort
downstream event emissions.

Artifact projection: pi's write/edit tool results now upsert a
`work_products` row via a new agent-runtime/artifact-projector.ts. No
more agents writing 54 files into the workspace that the UI never sees.
Migration v40 relaxes work_products.task_id to nullable so heartbeat
artifacts can register too.

File size: split agent-event-projection.ts out of runtime-bridge.ts to
keep the bridge under the 400-line cap after adding the projector hook.

Tests: 604 unit + integration green; generateHandoff callsites updated
for the single-argument signature; artifact-projector has pure-function
coverage via the projector module's own shape.
2026-04-18 21:37:56 -04:00
ZacharyZhang-NY c71d033b0c refactor: prune dead IPC surface and orphaned modules
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.
2026-04-17 22:38:10 -04:00
ZacharyZhang-NY fd6471988a refactor(app): remove Console feature entirely
Per user request: the "显示控制台 / Show console" button in the top-right
header and the bottom drawer it opened are no longer needed — the Runs
section already renders the same log tail in-page via RunDetail, so the
drawer was a duplicate surface. Deleted the entire feature instead of
leaving a hidden toggle that attracts bugs.

Removed files:
- src/renderer/views/ConsoleView.tsx (126 LOC)

Removed renderer integration points:
- App.tsx: header "Show/Hide console" button, ConsoleView render,
  setConsoleOpen(true) call after startRun, TerminalWindow icon import
- views/sections/ExecutionSections.tsx: onOpenConsole callback,
  setConsoleOpen subscription
- components/runs/RunCenter.tsx: onOpenConsole prop (removed from
  forwarding chain)
- components/runs/RunDetail.tsx: onOpenConsole prop + the Lightning
  "Open console" ActionButton. The cancel-run button remains as the
  sole Panel action now that the console entry point is gone.
- hooks/useUIController.ts: consoleOpen / setConsoleOpen subscription
- hooks/useDesktopEvents.ts: "toggle-console" shortcut dispatcher
- state/slices/navigation.ts: consoleOpen field, setConsoleOpen,
  toggleConsole actions, and the two auto-open-on-navigate triggers
  that used to flip the drawer when jumping to a run

Removed main-process integration:
- main/desktop-shell-menu.ts: "Toggle Console" menu item + CmdOrCtrl+`
  accelerator
- main/desktop-shell-window.ts: onShortcut union type narrowed
- shared/types/events.ts: ShortcutEvent action union narrowed

Removed i18n keys (zh + en):
- app.hideConsole / app.showConsole
- console.title / console.noRuns / console.close
- common.openLog / common.noRunSelected / common.waitingForLog
- runs.openConsole

Log capture itself (logBufferRef in useCompanyController + run-log
events) is unchanged — the Runs section still displays it. Nothing
was moved to a "disabled but present" state; the feature is out of
the tree entirely. Typecheck and lint pass.
2026-04-17 20:32:34 -04:00
ZacharyZhang-NY 36bed1b0f1 feat(team): add TeamHeaderSummary strip + delete unreachable orgchart section
Two S-level audit items landed together because they are the same
"first-second orientation" concern on the team page:

S1 — Empty OrgChart was the first thing the board saw; the canvas
     carried no summary data. Now a compact summary strip sits above
     the chart showing Members / Running / Attention / Pending hires /
     This-month budget (spent/cap/%). All five numbers computed from
     the existing snapshot — no extra IPC.
S9 — Activity and budget context previously required visiting three
     separate pages. The summary strip collapses the minimum-useful
     subset into a single glance; the full drill-down paths (Work,
     Approvals, Costs) remain one click away.

S10 — The `orgchart` SectionId was a dead rendering branch:
       - Not in the sidebar (useUIController sidebarGroups)
       - Not in the command palette (paletteOnlyNavDefs)
       - Not reachable from any navigateTo() call in renderer code
     The only callers were an automation post-hire notification and a
     unit test — both pointed at "orgchart" as a navigation target.
     Routed both to "agents" instead (which renders the OrgChart at
     the top anyway, plus the new summary strip and the roster), so
     clicking the "Agent Hired" notification now lands on the team
     page with the new hire highlighted.

Removed:
- `orgchart` from SectionId enum (src/shared/types/enums.ts)
- `section === "orgchart"` render branch (OrganizationSections.tsx)
- `nav.orgchart` i18n key (zh + en)

Added:
- src/renderer/components/organization/TeamHeaderSummary.tsx
- team.summary.* i18n keys (zh + en)

Covers S1 / S9 / S10.
2026-04-17 19:57:58 -04:00
ZacharyZhang-NY f012f8e880 refactor(skills): drop company_skills.markdown column, read from disk via IPC
`company_skills.markdown` stored the full SKILL.md body. The on-disk
file under `profile/skills/{companyId}/{slug}/SKILL.md` is what pi-
coding-agent actually reads via `loadSkills` at runtime. The DB column
was a duplicate — import wrote to both, renderer read from DB. Any
discrepancy between disk and DB was invisible until the agent behaved
differently from what the user saw in SkillDetail.

Disk becomes the single source of truth. DB keeps name / description /
metadata / source info / file inventory — the cheap lookup row. UI reads
SKILL.md content live via a new IPC channel.

## Migration v37 (rebuild company_skills without markdown)

Table-rebuild pattern matching v35/v36: `fn:` form, PRAGMA walk for
drift columns, row-count assertion before DROP, `defer_foreign_keys`
for the `agent_skill_assignments(skill_id)` reference. Indexes
recreated. Idempotent via `schema_migrations` ledger.

Existing DB rows' markdown content is discarded — but the on-disk
SKILL.md files are the authoritative copy, so no user-visible data
loss. Any agent using those skills keeps working; the UI just starts
reading from disk instead of the DB cache.

## Code changes

- `src/main/database/skills.ts`: drop `markdown` from read mapping,
  upsert input type, INSERT column/bind list, ON CONFLICT DO UPDATE.
- `src/shared/types/records-core.ts`: `SkillRecord.markdown` removed.
- `src/main/skills/import.ts` + `import-github.ts`: drop `markdown:`
  from 4 `saveCompanySkill` call sites. Disk-write via
  `materializeSkillFiles` unchanged (it's what the file agent reads).
- `src/main/skills/index.ts`: expose `skillFilesDir` for the new IPC
  handler to resolve `${profile}/skills/${companyId}/${slug}/SKILL.md`.
- `src/shared/contracts/channels.ts` + `schemas-execution.ts` +
  `desktop-api/skills.ts`: new `getSkillMarkdown` channel
  `{ companyId, slug } → DesktopResult<string>`.
- `src/preload/index.ts`: expose `getSkillMarkdown(companyId, slug)`.
- `src/main/ipc-skill-handlers.ts`: handler validates schema, ensures
  company, resolves path, `readFile`, returns content. Fails closed
  with `SKILL_NOT_FOUND` on ENOENT, `SKILL_READ_FAILED` on other
  read errors.
- `src/renderer/components/skills/SkillDetail.tsx`: `useEffect` fetches
  markdown via IPC when the skill detail loads; shows "Loading…" until
  resolved, surfaces read errors. Preserves `stripFrontmatter`.
- Tests: drop `markdown:` from 2 fixture sites
  (`skill-types.test.ts`, `skill-aware-assignment.spec.ts`).

## Verification

- typecheck / lint / 578 tests green
- `grep 'skill\.markdown' src/` → 0 DB-record reads
- `grep '\.markdown\b' src/main/database/skills.ts` → 0
- Remaining `.markdown` references are all `ghSkill.markdown` (GitHub
  fetcher's intermediate struct feeding `materializeSkillFiles` to
  disk) and parameter names in `stripFrontmatter(markdown: string)` /
  fs-writer helpers — none read from a DB row.
2026-04-17 16:22:31 -04:00
ZacharyZhang-NY 7e1d4199ee refactor(schema): make runs.task_id nullable, delete heartbeat workaround
The NOT NULL constraint forced heartbeat runs to synthesize a fake task
row (`[Heartbeat] X — Check for work`) so the FK could resolve. Three
layers of code — acquireHeartbeatTask, findReusableHeartbeatTask,
cleanupDuplicateHeartbeatTasks, plus the is_heartbeat flag column + UI
filter — existed to paper over one bad constraint. Remove the
constraint and the whole workaround vanishes.

SQLite table rebuild with data preservation. Live production DB, no
backup — migration is atomic, idempotent, and verified before DROP.

## Migration v35 (rebuild runs with nullable task_id)

Uses `fn:` form so it can:
- Snapshot `COUNT(*) FROM runs` before copy
- Walk `PRAGMA table_info(runs)` to detect drift columns (v34 declared
  a column set; any additive ALTER since v14 shows up here)
- Assert `COUNT(*) FROM runs_new == beforeCount` before `DROP TABLE runs`
- `PRAGMA defer_foreign_keys = ON` so run_events(run_id) FK defers to
  commit time, by which runs has the same id set under the new name
- Recreate all 6 indexes on the new table

If the assertion fails, the migration throws inside the existing
`transaction()` wrapper. SQLite rolls back the whole migration, old
`runs` table is untouched, `schema_migrations` never records v35. Next
app start retries cleanly.

## Migration v36 (drop tasks.is_heartbeat + synthetic rows)

Runs now carry `task_id = NULL` for heartbeat dispatches. The
`is_heartbeat` flag is dead data going forward.

1. `UPDATE runs SET task_id = NULL WHERE task_id IN (SELECT id FROM tasks WHERE is_heartbeat = 1)`
2. `DELETE FROM tasks WHERE is_heartbeat = 1` (synthetic rows, not real work)
3. Rebuild `tasks` without the `is_heartbeat` column (same defer-FK +
   row-count-assert pattern as v35)

## Code cleanup

- `RunRecord.taskId: string | null`; `createRun` / `cancelRunForDeletion`
  / `finishRun` guard every `UPDATE tasks` path with a null-check.
- `TaskRecord.isHeartbeat` removed; every SELECT / filter / UI reference
  dropped.
- Delete `acquireHeartbeatTask` / `findReusableHeartbeatTask` /
  `cleanupDuplicateHeartbeatTasks` — dead after the constraint lifts.
- Delete `src/main/orchestration/heartbeat-task.ts` (pure workaround
  constant file).
- `heartbeat.ts` creates runs with `taskId: null` when the agent has no
  assigned work. Turn context already supports nullable task
  (heartbeatFallbackSection); prompt surface unchanged.
- `validateTaskCreation` drops the `isHeartbeat` parameter — user-
  authored + automation-authored tasks are never heartbeat.
- Renderer: `run.taskId ? taskMap.get(...) : undefined` in Dashboard /
  RunCenter / RunDetail / RunQueue.
- Delete `tests/unit/heartbeat-coalescing.test.ts` (12 tests that
  exercised the deleted DB methods).
- Drop 1 isHeartbeat-bypass test from `task-quality-gates.test.ts`.

## Grep verification

- `isHeartbeatTaskTitle src/` → 0
- `acquireHeartbeatTask src/` → migration comment only
- `is_heartbeat src/` → schema.ts migration history only
- `isHeartbeat src/` → 0

Tests: 578/578 (was 591 — delta is 13 tests that tested deleted code).
2026-04-17 16:16:08 -04:00
ZacharyZhang-NY 92795f7bfb refactor(session): scope pi sessions to agents instead of tasks
Sessions were keyed by (task, agent). Every task switch started a fresh
transcript — the agent lost everything it had learned on the previous
task. That breaks the product premise: a CEO should remember its past
decisions, an engineer should carry context between bugs.

Move pi's AgentMessage transcript to be agent-scoped. One session per
agent (per company). Tasks enter the ongoing conversation as user turns;
rotation/compaction boundaries follow the agent's long-running session.

Additive schema only — no row rewrites on the live production DB.

- Migration v34: `ALTER TABLE agents ADD COLUMN session_id TEXT` +
  `idx_agents_session_id`. `ensureOptionalColumns` mirror for fresh DBs.
- `AgentRecord.sessionId?: string | null` in the read model; included
  in every full-agent SELECT (entity-crud, snapshot, agents-hierarchy
  chain-of-command / direct-reports / heartbeat-eligible).
- `db.ensureAgentSession({ agentId, companyId, adapterType })` —
  idempotent: returns existing `agents.session_id` if set, else inserts
  a new `task_sessions` row with an empty `task_id` sentinel (agent-
  scoped, task-id is now metadata) and stores the id on the agent.
- `db.setAgentSessionId(agentId, sessionId)` — used by rotation.
- `db.updateTasksActiveSessionForAgent(agentId, sessionId)` — mirrors
  the agent's current session onto every task the agent owns, so
  existing per-task readers (`tasks.active_session_id`) still work.
- `turn-context.resolveSession`: pulls the agent's session via
  `ensureAgentSession` instead of `getActiveTaskSession(task.id)`,
  then points the current task at it so UI continues to resolve.
- `session-management-service.rotateSession`: new session carries
  `taskId:""`, then `setAgentSessionId` + `updateTasksActiveSessionForAgent`
  so every task the agent owns follows the rotation boundary.
- `session-entries.listActive`: `ORDER BY created_at, sequence` — the
  created_at timestamp is authoritative; sequence stays as same-ms
  tiebreaker. Forward-safety for future merge scenarios.

Existing agents' first post-upgrade run creates a fresh agent-scoped
session. Their pre-migration per-task transcripts remain in the DB as
historical records but are not loaded for new runs. This is a one-time
transcript reset, accepted over a risky lossy merge on the live DB.

Documented in a code comment on `ensureAgentSession`.
2026-04-17 15:42:13 -04:00
ZacharyZhang-NY 5547c29491 update 2026-04-17 14:03:56 -04:00