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

+13 -2
View File
@@ -69,6 +69,7 @@ export type ArtifactProjectorRunContext = {
agentId: string;
companyId: string;
taskId: string | null;
workspaceId: string | null;
workspacePath: string | null;
};
@@ -284,14 +285,24 @@ function upsertFilesystemArtifact(
const title = lastSlash >= 0 ? relativePath.slice(lastSlash + 1) : relativePath;
const provider = "filesystem";
const existing = db.findWorkProductByExternalId(ctx.companyId, provider, relativePath);
// Identity is scoped to (company, workspace, provider, externalId) so two
// workspaces with the same relative path do not collapse onto one row.
// Every touch refreshes `last_touched_by_run_id` so the deliverable
// signal, the DocumentCenter preview, and the run-provenance readers
// all see the latest run that wrote the file.
const existing = db.findWorkProductByExternalId(ctx.companyId, provider, relativePath, ctx.workspaceId);
if (existing) {
db.updateWorkProduct(existing.id, ctx.companyId, { title });
db.updateWorkProduct(existing.id, ctx.companyId, {
title,
lastTouchedByRunId: ctx.runId,
workspaceId: ctx.workspaceId,
});
return { kind: "updated", workProductId: existing.id, relativePath };
}
const workProductId = db.createWorkProduct({
companyId: ctx.companyId,
taskId: ctx.taskId,
workspaceId: ctx.workspaceId,
type: "file",
provider,
externalId: relativePath,
+2
View File
@@ -56,6 +56,7 @@ export interface ActiveRunContext {
connectorId: string;
modelId: string;
taskSessionId: string;
workspaceId: string | null;
workspacePath: string | null;
state: ActiveRunLifecycleState;
}
@@ -176,6 +177,7 @@ export function handleAfterToolCall(
agentId: ctx.agentId,
companyId: ctx.companyId,
taskId: ctx.taskId,
workspaceId: ctx.workspaceId,
workspacePath: ctx.workspacePath,
};
try {
+3
View File
@@ -114,6 +114,7 @@ export class RuntimeBridge {
agentId: ctx.agentId,
companyId: ctx.companyId,
taskId: ctx.taskId,
workspaceId: ctx.workspaceId,
workspacePath: ctx.workspacePath,
});
const inserted = outcomes.filter((o) => o.kind === "inserted").length;
@@ -460,6 +461,7 @@ export class RuntimeBridge {
companyId: string;
taskId: string | null;
modelId: string;
workspaceId: string | null;
workspacePath: string | null;
};
this.registerActiveRun({
@@ -470,6 +472,7 @@ export class RuntimeBridge {
connectorId: payload.connectorId,
modelId: payload.modelId,
taskSessionId: payload.taskSessionId,
workspaceId: payload.workspaceId,
workspacePath: payload.workspacePath,
state: "preparing",
});