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.
This commit is contained in:
1 parent
61fb4b947f
commit
ff5e22092a
45 files changed
+3409
-300
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import log from "electron-log/main.js";
|
||||
import type { Migration, MigrationDeps } from "./types";
|
||||
import { columnExists, tableExists } from "./types";
|
||||
|
||||
export function createMigrationsV38V39(deps: MigrationDeps): Migration[] {
|
||||
const { db } = deps;
|
||||
return [
|
||||
{
|
||||
// Model capability table. Maps (provider, model_id) to 1..5 scores across
|
||||
// three domains (reasoning / coding / writing), a tier enum used as a
|
||||
// hard lower bound for task-profile matching, and a cost-per-Mtok-output
|
||||
// figure used to pick the cheapest qualifying model.
|
||||
//
|
||||
// Scores, tier, and cost are data — editable at runtime via IPC. Seed
|
||||
// rows are inserted by the main process on startup when the table is
|
||||
// empty so a fresh install boots with a sensible matrix covering the
|
||||
// providers in the pi-ai catalog and the curated Fireworks list.
|
||||
version: 38,
|
||||
name: "create_model_capabilities",
|
||||
fn: () => {
|
||||
if (tableExists(db, "model_capabilities")) {
|
||||
log.info("[migration v38] model_capabilities already exists; skipping");
|
||||
return;
|
||||
}
|
||||
db.exec(
|
||||
`CREATE TABLE model_capabilities (
|
||||
provider TEXT NOT NULL,
|
||||
model_id TEXT NOT NULL,
|
||||
reasoning INTEGER NOT NULL,
|
||||
coding INTEGER NOT NULL,
|
||||
writing INTEGER NOT NULL,
|
||||
tier TEXT NOT NULL,
|
||||
cost_per_mtok_out REAL NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (provider, model_id)
|
||||
)`,
|
||||
);
|
||||
db.exec("CREATE INDEX idx_model_capabilities_tier ON model_capabilities(tier)");
|
||||
log.info("[migration v38] created model_capabilities table");
|
||||
},
|
||||
},
|
||||
{
|
||||
// agents.model_strategy drives run-time model dispatch.
|
||||
//
|
||||
// 'explicit' — use agent.connector_id and agent.model verbatim
|
||||
// (the pre-v39 behavior — kept for existing rows so there
|
||||
// are no regressions on upgrade).
|
||||
// 'auto' — ignore agent.connector_id/model; at run dispatch time
|
||||
// pick the cheapest model whose capability vector meets
|
||||
// the agent's inferred task profile.
|
||||
//
|
||||
// New agents default to 'auto' at the application layer. Existing rows
|
||||
// on upgrade are forced to 'explicit' because the user previously picked
|
||||
// their model manually and we must not silently re-route them.
|
||||
version: 39,
|
||||
name: "add_agents_model_strategy",
|
||||
fn: () => {
|
||||
if (!tableExists(db, "agents")) {
|
||||
throw new Error("agents table missing; refusing to migrate v39");
|
||||
}
|
||||
if (columnExists(db, "agents", "model_strategy")) {
|
||||
log.info("[migration v39] agents.model_strategy already exists; skipping");
|
||||
return;
|
||||
}
|
||||
// Default at schema level = 'explicit' so existing rows keep their
|
||||
// hand-picked model. Application code uses 'auto' as the default for
|
||||
// freshly created agents.
|
||||
db.exec(
|
||||
"ALTER TABLE agents ADD COLUMN model_strategy TEXT NOT NULL DEFAULT 'explicit'",
|
||||
);
|
||||
log.info("[migration v39] added agents.model_strategy column");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import log from "electron-log/main.js";
|
||||
import type { Migration, MigrationDeps } from "./types";
|
||||
import { tableExists } from "./types";
|
||||
|
||||
export function createMigrationsV40V40(deps: MigrationDeps): Migration[] {
|
||||
const { db } = deps;
|
||||
return [
|
||||
{
|
||||
// Relax work_products.task_id from NOT NULL to NULL-allowed.
|
||||
//
|
||||
// Background: every file that an agent writes via pi's write/edit tool
|
||||
// should be registered as a work product so the desktop UI surfaces
|
||||
// it. Heartbeat (taskless) runs — CEO team-building scratch memos,
|
||||
// research exploration — legitimately produce files with no owning
|
||||
// task. Forcing task_id NOT NULL made the artifact projector skip
|
||||
// these runs, which in turn made the UI look empty while the
|
||||
// workspace filesystem filled up with unregistered output.
|
||||
//
|
||||
// SQLite has no ALTER COLUMN, so we rebuild the table. The operation
|
||||
// is done inside a transaction; PRAGMA foreign_keys is disabled for
|
||||
// the rebuild window and restored afterward so child rows do not
|
||||
// cascade-delete when the old table is dropped mid-swap.
|
||||
version: 40,
|
||||
name: "relax_work_products_task_id",
|
||||
fn: () => {
|
||||
if (!tableExists(db, "work_products")) {
|
||||
log.info("[migration v40] work_products table missing; skipping");
|
||||
return;
|
||||
}
|
||||
const info = db.prepare("PRAGMA table_info(work_products)").all() as Array<{
|
||||
name: string;
|
||||
notnull: number;
|
||||
}>;
|
||||
const taskIdRow = info.find((row) => row.name === "task_id");
|
||||
if (!taskIdRow) {
|
||||
throw new Error("work_products.task_id column missing; refusing to migrate v40");
|
||||
}
|
||||
if (taskIdRow.notnull === 0) {
|
||||
log.info("[migration v40] work_products.task_id already nullable; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
db.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE work_products_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
company_id TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
task_id TEXT,
|
||||
type TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
external_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
review_state TEXT NOT NULL DEFAULT 'none',
|
||||
is_primary INTEGER NOT NULL DEFAULT 0,
|
||||
summary TEXT,
|
||||
metadata_json TEXT,
|
||||
created_by_run_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO work_products_new (
|
||||
id, company_id, project_id, task_id, type, provider, external_id,
|
||||
title, url, status, review_state, is_primary, summary, metadata_json,
|
||||
created_by_run_id, created_at, updated_at
|
||||
)
|
||||
SELECT id, company_id, project_id, task_id, type, provider, external_id,
|
||||
title, url, status, review_state, is_primary, summary, metadata_json,
|
||||
created_by_run_id, created_at, updated_at
|
||||
FROM work_products;
|
||||
DROP TABLE work_products;
|
||||
ALTER TABLE work_products_new RENAME TO work_products;
|
||||
CREATE INDEX IF NOT EXISTS idx_work_products_task
|
||||
ON work_products(company_id, task_id, type);
|
||||
CREATE INDEX IF NOT EXISTS idx_work_products_updated
|
||||
ON work_products(company_id, updated_at);
|
||||
`);
|
||||
} finally {
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
log.info("[migration v40] relaxed work_products.task_id to nullable");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user