diff --git a/src/api/capacity/services/build/ready-execution-node.ts b/src/api/capacity/services/build/ready-execution-node.ts index 352e8113..50979fb6 100644 --- a/src/api/capacity/services/build/ready-execution-node.ts +++ b/src/api/capacity/services/build/ready-execution-node.ts @@ -204,8 +204,8 @@ async function effectiveProfile(store: any, node: ExecutionNode): Promise<{ proj `Ready node ${node.id} has no exact active ${node.agentClass} ${node.kind} profile.`, 409); } -async function predecessorResults(store: any, node: ExecutionNode): Promise { - const rows = await store.all(`SELECT result.assignment_result_json +async function predecessorContext(store: any, node: ExecutionNode): Promise<{ results: AssignmentResult[]; contentRefs: ExactEntityReference[] }> { + const rows = await store.all(`SELECT result.assignment_result_json,result.assignment_attempt_json FROM execution_edges edge JOIN execution_nodes predecessor ON predecessor.team_id=edge.team_id AND predecessor.id=edge.from_node_id JOIN capacity_provider_assignments result ON result.team_id=edge.team_id @@ -215,13 +215,13 @@ async function predecessorResults(store: any, node: ExecutionNode): Promise 1 && node.workItemId) rows.push(...await store.all( - `SELECT result.assignment_result_json FROM capacity_provider_assignments result + `SELECT result.assignment_result_json,result.assignment_attempt_json FROM capacity_provider_assignments result WHERE result.team_id=? AND result.execution_node_id=? AND result.execution_node_revision [result.id, result])).values()]; + const contentRefs = rows.flatMap((row: Row) => { + const parsed = assignmentResultSchema.safeParse(record(row.assignment_result_json)); + if (!parsed.success) return []; + const grants = array(record(record(row.assignment_attempt_json).grant).contentWrite).map(record); + return parsed.data.references.flatMap((reference) => { + if (reference.kind !== 'treedx') return []; + const target = grants.find(grant => grant.store === 'treedx' && grant.repository === reference.repository && grant.path === reference.path); + if (!target || !text(target.model) || !text(target.id)) return []; + return [{ store: 'treedx' as const, model: text(target.model), id: text(target.id), + repository: reference.repository, commit: reference.commit, path: reference.path }]; + }); + }); + return { results: [...new Map(results.map((result) => [result.id, result])).values()], + contentRefs: [...new Map(contentRefs.map(reference => [stable(reference), reference])).values()] }; } async function teamCoreContext(store: any, teamId: string): Promise { @@ -307,7 +320,8 @@ export async function listReadyExecutionNodes(store: any, run: DurableCapacityWo ? [selectAssignmentSourceRepository(await store.listHubRepositories(node.projectId)).id] : []; const loadedContext = await loadContext(store, node); - const predecessors = await predecessorResults(store, node); + const predecessor = await predecessorContext(store, node); + const predecessors = predecessor.results; const candidateRefs = predecessors.flatMap((result) => result.references).flatMap((reference) => { if (reference.kind !== 'git') return []; const declared = loadedContext.find((item) => item.store === 'git' && item.repository === reference.repository); @@ -319,7 +333,7 @@ export async function listReadyExecutionNodes(store: any, run: DurableCapacityWo projectAgentClassId: selected.projectAgentClassId, effectiveProfile: selected.profile, sourceRepositories, - contextRefs: [...new Map([node.sourceRef, ...(node.authorityRefs ?? []), ...teamContext, ...projectContext, ...candidateRefs, ...loadedContext] + contextRefs: [...new Map([node.sourceRef, ...(node.authorityRefs ?? []), ...teamContext, ...projectContext, ...candidateRefs, ...predecessor.contentRefs, ...loadedContext] .filter((reference) => reference.store === 'git' ? Boolean(reference.repository && reference.commit) : reference.store === 'treedx' && Boolean(reference.repository && reference.commit && reference.path)) diff --git a/tests/unit/control-plane/capacity/execution/ready-execution-node.test.ts b/tests/unit/control-plane/capacity/execution/ready-execution-node.test.ts index 212341ff..32bd1357 100644 --- a/tests/unit/control-plane/capacity/execution/ready-execution-node.test.ts +++ b/tests/unit/control-plane/capacity/execution/ready-execution-node.test.ts @@ -159,6 +159,27 @@ describe('direct ready-node admission input', () => { expect(JSON.stringify(candidate)).not.toMatch(/capacityPlan|demand|sourceCandidate|artifactManifest/u); }); + it.each(['book', 'knowledge'])('loads the exact %s candidate using its producing assignment grant', async (model) => { + const target = { store: 'treedx', model, id: 'actor-output', repository: 'repository', + commit: 'e'.repeat(40), path: `${model}s/actor-output.mdx` }; + const actorResult = { ...result, references: [{ kind: 'treedx', projectId, + repository: target.repository, commit: 'd'.repeat(40), path: target.path }] }; + const reviewPermissions = { content: { read: ['proposal', 'decision', model], write: ['decision'] }, tools: ['source.read'] }; + const reviewer = { ...definition, id: 'agent:reviewer', agentClass: 'reviewer', + activityProfiles: { reviewing: { handler: 'reviewer', permissions: reviewPermissions, + prompt: { system: 'Review the exact immutable actor output.' } } } }; + const node = { ...nodeRow(), kind: 'reviewing', pair_role: 'reviewer', agent_class: 'reviewer', + workspace: 'treedx', requested_permissions_json: reviewPermissions }; + const createStore = (grants: unknown[]) => ({ ...teamContextStore, all: vi.fn() + .mockResolvedValueOnce([node]) + .mockResolvedValueOnce([{ id: 'class-reviewer', handler_refs_json: { agents: [reviewer] } }]) + .mockResolvedValueOnce([{ assignment_result_json: actorResult, assignment_attempt_json: { grant: { contentWrite: grants } } }]) }); + const [candidate] = await listReadyExecutionNodes(createStore([target]), run as never, project as never, async () => contextRefs); + expect(candidate.contextRefs).toContainEqual({ ...target, commit: 'd'.repeat(40) }); + const [denied] = await listReadyExecutionNodes(createStore([{ ...target, path: 'other.mdx' }]), run as never, project as never, async () => contextRefs); + expect(denied.contextRefs).not.toContainEqual(expect.objectContaining({ model, commit: 'd'.repeat(40) })); + }); + it('uses an exact resolved commit when the configured content ref is a branch', async () => { const store = { ...teamContextStore, getProjectTreeDxLibrary: vi.fn(async () => ({ repositoryId: 'team-repository',