{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/missions/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,OAAO,EAAmB,MAAM,oBAAoB,CAAC;AAWnE,OAAO,KAAK,EAEX,aAAa,EAIb,kBAAkB,EAClB,oBAAoB,EACpB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,GAAG;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC1F;AAED,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,WAAW,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA+BD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC3C,MAAM,EAAE,mBAAmB,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,oBAAoB,GAAG,SAAS,CAmCnC;AAsFD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IAClD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,eAAe,CAAC,OAAO,CAAC,CAoG3B;AA6BD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAIrF;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAuFxF","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@lpb-work/pi-agent-core\";\nimport { writePrivateAtomicJson } from \"../shared/atomic-json.ts\";\nimport type { Details, SubagentRunMode } from \"../shared/types.ts\";\nimport { validateMissionLaunch } from \"./actions.ts\";\nimport {\n\tcreateMission,\n\tMissionNotFoundError,\n\tmissionRecordPath,\n\treadMission,\n\tresolveMissionStoreLocation,\n\tupdateMission,\n\tvalidateMissionId,\n} from \"./store.ts\";\nimport type {\n\tMissionArtifact,\n\tMissionRecord,\n\tMissionRunLink,\n\tMissionRunMode,\n\tMissionStatus,\n\tMissionStoreConfig,\n\tMissionStoreLocation,\n} from \"./types.ts\";\n\nexport const MISSION_BINDING_FILE = \"mission.json\";\n\nexport interface MissionLaunchParams {\n\tmissionId?: string;\n\tmission?: unknown;\n\ttask?: string;\n\ttasks?: Array<{ task?: string }>;\n\tchain?: Array<{ task?: string; parallel?: Array<{ task?: string }> | { task?: string } }>;\n}\n\nexport interface MissionLaunchBinding {\n\tmissionId: string;\n\tlocation: MissionStoreLocation;\n\tautoCreated: boolean;\n\tannounceInContent?: boolean;\n}\n\ninterface PersistedMissionBinding {\n\tschemaVersion: 1;\n\tmissionId: string;\n\tprojectRoot: string;\n\tmissionDir: string;\n\tglobalIndexDir: string;\n\twriteGlobalIndex: boolean;\n\tretainTerminal?: number;\n}\n\nfunction workflowObjective(params: MissionLaunchParams): string | undefined {\n\tconst objective =\n\t\tparams.task?.trim() ||\n\t\tparams.tasks?.find((task) => task.task?.trim())?.task?.trim() ||\n\t\tparams.chain?.find((step) => step.task?.trim())?.task?.trim();\n\tif (objective) return objective;\n\tfor (const step of params.chain ?? []) {\n\t\tconst parallel = Array.isArray(step.parallel) ? step.parallel : step.parallel ? [step.parallel] : [];\n\t\tconst task = parallel.find((child) => child.task?.trim())?.task?.trim();\n\t\tif (task) return task;\n\t}\n\treturn undefined;\n}\n\nfunction conciseTitle(objective: string): string {\n\tconst firstLine = objective.split(/\\r?\\n/, 1)[0]?.trim() || objective.trim();\n\treturn firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;\n}\n\nexport function prepareMissionLaunch(input: {\n\tparams: MissionLaunchParams;\n\tprojectRoot: string;\n\tconfig?: MissionStoreConfig;\n\townerSessionId?: string;\n}): MissionLaunchBinding | undefined {\n\tconst hasMissionId = input.params.missionId !== undefined;\n\tif (hasMissionId && input.params.mission !== undefined) throw new Error(\"Use missionId or mission, not both\");\n\tif (input.params.mission === false) return undefined;\n\tconst objective = workflowObjective(input.params);\n\tconst missionsEnabled = input.config?.enabled !== false;\n\tconst shouldCreate = input.params.mission !== undefined || (missionsEnabled && objective !== undefined);\n\tif (!hasMissionId && !shouldCreate) return undefined;\n\tconst location = resolveMissionStoreLocation({\n\t\tprojectRoot: input.projectRoot,\n\t\t...(input.config ? { config: input.config } : {}),\n\t});\n\tif (hasMissionId) {\n\t\tconst missionId = validateMissionId(input.params.missionId);\n\t\treadMission(location, missionId);\n\t\tupdateMission(location, missionId, { status: \"active\" });\n\t\treturn { missionId, location, autoCreated: false, announceInContent: true };\n\t}\n\tconst mission = input.params.mission !== undefined ? validateMissionLaunch(input.params.mission) : undefined;\n\tconst title = mission?.title || conciseTitle(objective!);\n\tconst record = createMission(\n\t\tlocation,\n\t\t{\n\t\t\ttitle,\n\t\t\tobjective: mission?.objective || objective || title,\n\t\t\t...(mission?.goal === true ? { goal: true as const } : {}),\n\t\t\t...(mission?.budget ? { budget: mission.budget } : {}),\n\t\t\tstatus: \"active\",\n\t\t\t...(mission?.labels ? { labels: mission.labels } : {}),\n\t\t\t...(input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}),\n\t\t},\n\t\tnew Date(),\n\t\tinput.config?.retainTerminal,\n\t);\n\treturn { missionId: record.id, location, autoCreated: true, announceInContent: true };\n}\n\nfunction toolResultIsError(result: AgentToolResult<Details>): boolean {\n\treturn \"isError\" in result && result.isError === true;\n}\n\nfunction missionRunModeForResult(mode: Details[\"mode\"]): MissionRunMode {\n\treturn mode === \"management\" ? \"external\" : mode;\n}\n\nfunction runStatusForResult(result: AgentToolResult<Details>): string {\n\tif (result.details.asyncDir) return \"active\";\n\tif (result.details.results.some((child) => child.interrupted || child.detached)) return \"paused\";\n\tif (toolResultIsError(result) || result.details.results.some((child) => child.exitCode !== 0)) return \"failed\";\n\treturn \"completed\";\n}\n\nfunction missionStatusForRun(record: MissionRecord, runId: string, runStatus: string): MissionStatus {\n\tif (record.status === \"completed\" || record.status === \"failed\" || record.status === \"cancelled\")\n\t\treturn record.status;\n\tif (runStatus === \"active\" || runStatus === \"queued\" || runStatus === \"running\") return \"active\";\n\tif (record.goal) return \"active\";\n\tif (runStatus === \"paused\") return \"waiting\";\n\tconst otherActive = record.runs.some(\n\t\t(run) => run.runId !== runId && (run.status === \"active\" || run.status === \"queued\" || run.status === \"running\"),\n\t);\n\tif (otherActive) return \"active\";\n\tif (runStatus === \"completed\" || runStatus === \"complete\") return \"completed\";\n\tif (runStatus === \"stopped\" || runStatus === \"rejected\" || runStatus === \"cancelled\") return \"cancelled\";\n\treturn \"failed\";\n}\n\nfunction usageForResult(result: AgentToolResult<Details>): { tokens: number } | undefined {\n\tconst tokens = result.details.results.reduce((total, child) => total + child.usage.input + child.usage.output, 0);\n\treturn tokens > 0 ? { tokens } : undefined;\n}\n\nfunction usageFromUnknown(value: unknown): { tokens: number } | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst total = (value as { total?: unknown }).total;\n\treturn Number.isSafeInteger(total) && (total as number) >= 0 ? { tokens: total as number } : undefined;\n}\n\nfunction artifactsForResult(result: AgentToolResult<Details>): MissionArtifact[] {\n\tconst artifacts: MissionArtifact[] = [];\n\tif (result.details.asyncDir) {\n\t\tartifacts.push(\n\t\t\t{ kind: \"status\", path: path.join(result.details.asyncDir, \"status.json\") },\n\t\t\t{ kind: \"other\", path: path.join(result.details.asyncDir, \"events.jsonl\"), description: \"Lifecycle events\" },\n\t\t);\n\t}\n\tfor (const child of result.details.results) {\n\t\tif (child.artifactPaths?.outputPath) artifacts.push({ kind: \"output\", path: child.artifactPaths.outputPath });\n\t\tif (child.savedOutputPath) artifacts.push({ kind: \"output\", path: child.savedOutputPath });\n\t\tif (child.transcriptPath)\n\t\t\tartifacts.push({ kind: \"other\", path: child.transcriptPath, description: \"Child transcript\" });\n\t\tif (child.structuredOutputPath)\n\t\t\tartifacts.push({ kind: \"output\", path: child.structuredOutputPath, description: \"Structured output\" });\n\t}\n\tif (result.details.parallelHandoff?.path)\n\t\tartifacts.push({ kind: \"manifest\", path: result.details.parallelHandoff.path });\n\treturn artifacts;\n}\n\nfunction firstText(result: AgentToolResult<Details>): string | undefined {\n\tconst text = result.content.find((item) => item.type === \"text\")?.text.trim();\n\tif (!text) return undefined;\n\treturn text.length > 2000 ? `${text.slice(0, 1997)}...` : text;\n}\n\nfunction persistedBinding(binding: MissionLaunchBinding): PersistedMissionBinding {\n\treturn {\n\t\tschemaVersion: 1,\n\t\tmissionId: binding.missionId,\n\t\tprojectRoot: binding.location.projectRoot,\n\t\tmissionDir: binding.location.missionDir,\n\t\tglobalIndexDir: binding.location.globalIndexDir,\n\t\twriteGlobalIndex: binding.location.writeGlobalIndex,\n\t\t...(binding.location.retainTerminal !== undefined ? { retainTerminal: binding.location.retainTerminal } : {}),\n\t};\n}\n\nfunction writeAsyncBinding(asyncDir: string, binding: MissionLaunchBinding): void {\n\twritePrivateAtomicJson(path.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));\n}\n\nexport function attachMissionToLaunchResult(input: {\n\tbinding: MissionLaunchBinding;\n\tresult: AgentToolResult<Details>;\n}): AgentToolResult<Details> {\n\tconst runId = input.result.details.runId ?? input.result.details.asyncId;\n\tif (!runId) {\n\t\tconst current = readMission(input.binding.location, input.binding.missionId);\n\t\tconst activeRunExists = current.runs.some(\n\t\t\t(run) => run.status === \"active\" || run.status === \"queued\" || run.status === \"running\",\n\t\t);\n\t\tconst mission = toolResultIsError(input.result)\n\t\t\t? updateMission(input.binding.location, input.binding.missionId, {\n\t\t\t\t\tstatus: activeRunExists ? \"active\" : \"failed\",\n\t\t\t\t\t...(firstText(input.result) ? { summary: firstText(input.result)! } : {}),\n\t\t\t\t})\n\t\t\t: current;\n\t\treturn {\n\t\t\t...input.result,\n\t\t\tdetails: {\n\t\t\t\t...input.result.details,\n\t\t\t\tmissionId: input.binding.missionId,\n\t\t\t\tmissionPath: missionRecordPath(input.binding.location, input.binding.missionId),\n\t\t\t\tmission,\n\t\t\t},\n\t\t};\n\t}\n\tconst runStatus = runStatusForResult(input.result);\n\tconst current = readMission(input.binding.location, input.binding.missionId);\n\tconst startedAt = new Date().toISOString();\n\tconst usage = usageForResult(input.result);\n\tconst run: MissionRunLink = {\n\t\trunId,\n\t\tmode: missionRunModeForResult(input.result.details.mode),\n\t\tstatus: runStatus,\n\t\tstartedAt,\n\t\t...(input.result.details.asyncDir ? { asyncDir: input.result.details.asyncDir } : {}),\n\t\t...(input.result.details.results.length === 1 && input.result.details.results[0]?.agent\n\t\t\t? { agent: input.result.details.results[0].agent }\n\t\t\t: {}),\n\t\t...(runStatus !== \"active\" ? { completedAt: startedAt } : {}),\n\t\t...(usage ? { usage } : {}),\n\t};\n\tlet mission = updateMission(input.binding.location, input.binding.missionId, {\n\t\tstatus: missionStatusForRun(current, runId, runStatus),\n\t\taddRuns: [run],\n\t\taddArtifacts: artifactsForResult(input.result),\n\t\t...(firstText(input.result) && runStatus !== \"active\" ? { summary: firstText(input.result)! } : {}),\n\t\t...(input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance\n\t\t\t? { acceptance: input.result.details.results[0].acceptance }\n\t\t\t: {}),\n\t});\n\tif (input.result.details.asyncDir) {\n\t\twriteAsyncBinding(input.result.details.asyncDir, input.binding);\n\t\tconst statusPath = path.join(input.result.details.asyncDir, \"status.json\");\n\t\tif (fs.existsSync(statusPath)) {\n\t\t\ttry {\n\t\t\t\tconst status = JSON.parse(fs.readFileSync(statusPath, \"utf-8\")) as { state?: unknown };\n\t\t\t\tif (typeof status.state === \"string\" && ![\"queued\", \"running\"].includes(status.state)) {\n\t\t\t\t\tmission =\n\t\t\t\t\t\tsyncMissionFromAsyncCompletion({\n\t\t\t\t\t\t\trunId,\n\t\t\t\t\t\t\tasyncDir: input.result.details.asyncDir,\n\t\t\t\t\t\t\tmode: input.result.details.mode,\n\t\t\t\t\t\t\tstate: status.state,\n\t\t\t\t\t\t\t...(firstText(input.result) ? { summary: firstText(input.result) } : {}),\n\t\t\t\t\t\t}) ?? mission;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to reconcile mission from terminal async status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\tconst lastTextIndex = input.result.content.findLastIndex((item) => item.type === \"text\");\n\tconst hasStructuredOutput = input.result.details.results.some((child) => child.structuredOutputPath !== undefined);\n\tconst textIsJson =\n\t\tlastTextIndex >= 0 &&\n\t\t(() => {\n\t\t\ttry {\n\t\t\t\tJSON.parse((input.result.content[lastTextIndex] as { type: \"text\"; text: string }).text);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t})();\n\treturn {\n\t\t...input.result,\n\t\tcontent:\n\t\t\tinput.binding.announceInContent === true && lastTextIndex >= 0 && !hasStructuredOutput && !textIsJson\n\t\t\t\t? input.result.content.map((item, index) =>\n\t\t\t\t\t\tindex === lastTextIndex && item.type === \"text\"\n\t\t\t\t\t\t\t? { ...item, text: `${item.text}\\nMission: ${mission.id} (${mission.status})` }\n\t\t\t\t\t\t\t: item,\n\t\t\t\t\t)\n\t\t\t\t: input.result.content,\n\t\tdetails: {\n\t\t\t...input.result.details,\n\t\t\tmissionId: mission.id,\n\t\t\tmissionPath: missionRecordPath(input.binding.location, mission.id),\n\t\t\tmission,\n\t\t},\n\t};\n}\n\nfunction parsePersistedBinding(value: unknown, source: string): MissionLaunchBinding {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`${source} must be an object`);\n\tconst input = value as Record<string, unknown>;\n\tif (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);\n\tfor (const field of [\"projectRoot\", \"missionDir\", \"globalIndexDir\"] as const) {\n\t\tif (typeof input[field] !== \"string\" || !input[field].trim())\n\t\t\tthrow new Error(`${source}.${field} must be a non-empty string`);\n\t}\n\tif (typeof input.writeGlobalIndex !== \"boolean\") throw new Error(`${source}.writeGlobalIndex must be boolean`);\n\tif (\n\t\tinput.retainTerminal !== undefined &&\n\t\t(!Number.isInteger(input.retainTerminal) || (input.retainTerminal as number) < 1)\n\t)\n\t\tthrow new Error(`${source}.retainTerminal must be a positive integer`);\n\treturn {\n\t\tmissionId: validateMissionId(input.missionId, `${source}.missionId`),\n\t\tautoCreated: false,\n\t\tlocation: {\n\t\t\tprojectRoot: input.projectRoot as string,\n\t\t\tmissionDir: input.missionDir as string,\n\t\t\tglobalIndexDir: input.globalIndexDir as string,\n\t\t\twriteGlobalIndex: input.writeGlobalIndex,\n\t\t\t...(input.retainTerminal !== undefined ? { retainTerminal: input.retainTerminal as number } : {}),\n\t\t},\n\t};\n}\n\nexport function readMissionBinding(asyncDir: string): MissionLaunchBinding | undefined {\n\tconst bindingPath = path.join(asyncDir, MISSION_BINDING_FILE);\n\tif (!fs.existsSync(bindingPath)) return undefined;\n\treturn parsePersistedBinding(JSON.parse(fs.readFileSync(bindingPath, \"utf-8\")), bindingPath);\n}\n\nexport function syncMissionFromAsyncCompletion(value: unknown): MissionRecord | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst event = value as Record<string, unknown>;\n\tif (typeof event.asyncDir !== \"string\" || !event.asyncDir.trim()) return undefined;\n\tconst binding = readMissionBinding(event.asyncDir);\n\tif (!binding) return undefined;\n\tconst runId = typeof event.runId === \"string\" ? event.runId : typeof event.id === \"string\" ? event.id : undefined;\n\tif (!runId) throw new Error(\"Async mission completion is missing runId\");\n\tconst runStatus = typeof event.state === \"string\" ? event.state : event.success === true ? \"completed\" : \"failed\";\n\tlet current: MissionRecord;\n\ttry {\n\t\tcurrent = readMission(binding.location, binding.missionId);\n\t} catch (error) {\n\t\tif (!(error instanceof MissionNotFoundError)) throw error;\n\t\ttry {\n\t\t\tfs.appendFileSync(\n\t\t\t\tpath.join(event.asyncDir, \"events.jsonl\"),\n\t\t\t\t`${JSON.stringify({\n\t\t\t\t\ttype: \"subagent.mission.sync.skipped\",\n\t\t\t\t\tts: Date.now(),\n\t\t\t\t\trunId,\n\t\t\t\t\tmissionId: binding.missionId,\n\t\t\t\t\treason: \"mission-record-missing\",\n\t\t\t\t\tmissionPath: missionRecordPath(binding.location, binding.missionId),\n\t\t\t\t})}\\n`,\n\t\t\t\t\"utf-8\",\n\t\t\t);\n\t\t} catch {\n\t\t\t// Mission bookkeeping is secondary to preserving the completed async result.\n\t\t}\n\t\treturn undefined;\n\t}\n\tconst completedAt = new Date().toISOString();\n\tconst artifacts: MissionArtifact[] = [\n\t\t{ kind: \"status\", path: path.join(event.asyncDir, \"status.json\") },\n\t\t{ kind: \"other\", path: path.join(event.asyncDir, \"events.jsonl\"), description: \"Lifecycle events\" },\n\t];\n\tif (\n\t\tevent.parallelHandoff &&\n\t\ttypeof event.parallelHandoff === \"object\" &&\n\t\ttypeof (event.parallelHandoff as { path?: unknown }).path === \"string\"\n\t) {\n\t\tartifacts.push({ kind: \"manifest\", path: (event.parallelHandoff as { path: string }).path });\n\t}\n\tif (Array.isArray(event.results)) {\n\t\tfor (const result of event.results) {\n\t\t\tif (!result || typeof result !== \"object\") continue;\n\t\t\tconst child = result as Record<string, unknown>;\n\t\t\tif (typeof child.artifactPath === \"string\") artifacts.push({ kind: \"output\", path: child.artifactPath });\n\t\t\tif (\n\t\t\t\tchild.artifactPaths &&\n\t\t\t\ttypeof child.artifactPaths === \"object\" &&\n\t\t\t\ttypeof (child.artifactPaths as { outputPath?: unknown }).outputPath === \"string\"\n\t\t\t) {\n\t\t\t\tartifacts.push({ kind: \"output\", path: (child.artifactPaths as { outputPath: string }).outputPath });\n\t\t\t}\n\t\t}\n\t}\n\tconst summary = typeof event.summary === \"string\" && event.summary.trim() ? event.summary.slice(0, 2000) : undefined;\n\tconst usage =\n\t\tusageFromUnknown(event.totalTokens) ??\n\t\t(Array.isArray(event.results)\n\t\t\t? {\n\t\t\t\t\ttokens: event.results.reduce((total, result) => {\n\t\t\t\t\t\tif (!result || typeof result !== \"object\") return total;\n\t\t\t\t\t\treturn total + (usageFromUnknown((result as { tokens?: unknown }).tokens)?.tokens ?? 0);\n\t\t\t\t\t}, 0),\n\t\t\t\t}\n\t\t\t: undefined);\n\treturn updateMission(binding.location, binding.missionId, {\n\t\tstatus: missionStatusForRun(current, runId, runStatus),\n\t\taddRuns: [\n\t\t\t{\n\t\t\t\trunId,\n\t\t\t\tmode:\n\t\t\t\t\ttypeof event.mode === \"string\" && [\"single\", \"parallel\", \"chain\"].includes(event.mode)\n\t\t\t\t\t\t? (event.mode as SubagentRunMode)\n\t\t\t\t\t\t: \"external\",\n\t\t\t\tasyncDir: event.asyncDir,\n\t\t\t\tstatus: runStatus,\n\t\t\t\tcompletedAt,\n\t\t\t\t...(usage && usage.tokens > 0 ? { usage } : {}),\n\t\t\t},\n\t\t],\n\t\taddArtifacts: artifacts,\n\t\t...(summary ? { summary } : {}),\n\t});\n}\n"]}