| 240 | } |
| 241 | |
| 242 | export async function upsertSubagentReportArtifact(params: { |
| 243 | /** Workspace id that owns the session dir we're writing into (used for file locking). */ |
| 244 | workspaceId: string; |
| 245 | workspaceSessionDir: string; |
| 246 | childTaskId: string; |
| 247 | parentWorkspaceId: string; |
| 248 | ancestorWorkspaceIds: string[]; |
| 249 | reportMarkdown: string; |
| 250 | /** Task-level model string used when running the sub-agent (optional for legacy entries). */ |
| 251 | model?: string; |
| 252 | /** Task-level thinking/reasoning level used when running the sub-agent (optional for legacy entries). */ |
| 253 | thinkingLevel?: ThinkingLevel; |
| 254 | workflowOwnedAncestorWorkspaceIds?: string[]; |
| 255 | planFilePath?: string; |
| 256 | structuredOutput?: unknown; |
| 257 | title?: string; |
| 258 | nowMs?: number; |
| 259 | }): Promise<SubagentReportArtifactIndexEntry> { |
| 260 | let updated: SubagentReportArtifactIndexEntry | null = null; |
| 261 | |
| 262 | await workspaceFileLocks.withLock(params.workspaceId, async () => { |
| 263 | const nowMs = params.nowMs ?? Date.now(); |
| 264 | |
| 265 | const model = |
| 266 | typeof params.model === "string" && params.model.trim().length > 0 |
| 267 | ? params.model.trim() |
| 268 | : undefined; |
| 269 | const thinkingLevel = coerceThinkingLevel(params.thinkingLevel); |
| 270 | |
| 271 | const planFilePath = |
| 272 | typeof params.planFilePath === "string" && params.planFilePath.trim().length > 0 |
| 273 | ? params.planFilePath.trim() |
| 274 | : undefined; |
| 275 | |
| 276 | const file = await readSubagentReportArtifactsFile(params.workspaceSessionDir); |
| 277 | const existing = file.artifactsByChildTaskId[params.childTaskId] ?? null; |
| 278 | const createdAtMs = existing?.createdAtMs ?? nowMs; |
| 279 | |
| 280 | // Write the report payload first so we never publish an index entry without a report body. |
| 281 | const reportPath = getSubagentReportArtifactPath( |
| 282 | params.workspaceSessionDir, |
| 283 | params.childTaskId |
| 284 | ); |
| 285 | try { |
| 286 | await fsPromises.mkdir(path.dirname(reportPath), { recursive: true }); |
| 287 | await writeFileAtomic( |
| 288 | reportPath, |
| 289 | JSON.stringify( |
| 290 | { |
| 291 | childTaskId: params.childTaskId, |
| 292 | parentWorkspaceId: params.parentWorkspaceId, |
| 293 | createdAtMs, |
| 294 | updatedAtMs: nowMs, |
| 295 | model, |
| 296 | thinkingLevel, |
| 297 | title: params.title, |
| 298 | ancestorWorkspaceIds: params.ancestorWorkspaceIds, |
| 299 | workflowOwnedAncestorWorkspaceIds: params.workflowOwnedAncestorWorkspaceIds, |