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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 15:43:48 -04:00
1 parent d1483dcd5d
commit d9b35d73ab
12 files changed
+179 -25

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
import log from "electron-log/main.js";
import type { Migration, MigrationDeps } from "./types";
import { columnExists, tableExists } from "./types";
export function createMigrationsV45V45(deps: MigrationDeps): Migration[] {
const { db } = deps;
return [
{
// Promote work_products identity from `(company_id, provider, external_id)`
// to `(company_id, workspace_id, provider, external_id)` and replace
// the write-once `created_by_run_id` provenance with an append-only
// `last_touched_by_run_id` so the deliverable judgment, the thread
// projection, and the DocumentCenter open link all stay correct as
// a file is rewritten across multiple runs.
//
// Pre-v45 behaviour silently collapsed two workspaces' files at the
// same relative path into one row, and locked provenance at the
// first run that touched the path — later edits were invisible to
// `runProducedDeliverable`, so the task that produced the edit
// could stay pinned in-progress.
//
// Backfill strategy: derive `workspace_id` per row via `task_id ->
// tasks.workspace_id` first, then via `created_by_run_id ->
// runs.workspace_id`. Rows with neither binding (historically the
// exception, not the rule) remain NULL so nothing is dropped; the
// new uniqueness constraint is scoped to rows with a workspace so
// legacy NULL rows stay live without triggering conflicts.
// `last_touched_by_run_id` is seeded to `created_by_run_id` so the
// deliverable signal reads the same provenance pre- and post-
// migration until the next write arrives.
version: 45,
name: "work_products_workspace_identity",
fn: () => {
if (!tableExists(db, "work_products")) {
log.info("[migration v45] work_products table missing; skipping");
return;
}
if (!columnExists(db, "work_products", "workspace_id")) {
db.exec("ALTER TABLE work_products ADD COLUMN workspace_id TEXT");
}
if (!columnExists(db, "work_products", "last_touched_by_run_id")) {
db.exec("ALTER TABLE work_products ADD COLUMN last_touched_by_run_id TEXT");
}
db.exec(`
UPDATE work_products
SET workspace_id = (
SELECT tasks.workspace_id FROM tasks
WHERE tasks.id = work_products.task_id
)
WHERE workspace_id IS NULL
AND task_id IS NOT NULL
`);
db.exec(`
UPDATE work_products
SET workspace_id = (
SELECT runs.workspace_id FROM runs
WHERE runs.id = work_products.created_by_run_id
)
WHERE workspace_id IS NULL
AND created_by_run_id IS NOT NULL
`);
db.exec(`
UPDATE work_products
SET last_touched_by_run_id = created_by_run_id
WHERE last_touched_by_run_id IS NULL
AND created_by_run_id IS NOT NULL
`);
db.exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_products_identity
ON work_products(company_id, workspace_id, provider, external_id)
WHERE workspace_id IS NOT NULL AND external_id IS NOT NULL
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_work_products_last_touched
ON work_products(company_id, last_touched_by_run_id)
`);
log.info("[migration v45] work_products identity + provenance upgraded");
},
},
];
}