{"version":3,"file":"steering.d.ts","sourceRoot":"","sources":["../../../../src/runs/background/steering.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { writePrivateAtomicJson } from \"../../shared/atomic-json.ts\";\nimport type {\n\tAsyncStatus,\n\tResolvedToolBudget,\n\tResolvedTurnBudget,\n\tSteerActionResult,\n\tSteeringRecoveryDescriptor,\n\tSteeringRequestStatus,\n\tSteeringStatus,\n\tSteeringTargetState,\n\tSteeringTargetStatus,\n} from \"../../shared/types.ts\";\nimport { readStatus } from \"../../shared/utils.ts\";\n\nexport const MAX_STEERING_REQUESTS = 20;\nexport const STEERING_MESSAGE_PREVIEW_LIMIT = 160;\n\nexport function createSteeringStatus(): SteeringStatus {\n\treturn { requested: 0, scheduled: 0, pending: 0, delivered: 0, failed: 0, recovered: 0, recent: [] };\n}\n\nexport function steeringStatus(status: Pick<AsyncStatus, \"steering\">): SteeringStatus {\n\treturn status.steering ?? createSteeringStatus();\n}\n\nexport function recordSteeringRequest(\n\tstatus: SteeringStatus,\n\tinput: {\n\t\tid: string;\n\t\trequestedAt: number;\n\t\tsource?: string;\n\t\tmessage: string;\n\t\ttargets: Array<{ index: number; state: SteeringTargetState; reason?: string }>;\n\t},\n): SteeringRequestStatus {\n\tconst existing = status.recent.find((request) => request.id === input.id);\n\tif (existing) return existing;\n\tconst request: SteeringRequestStatus = {\n\t\tid: input.id,\n\t\trequestedAt: input.requestedAt,\n\t\t...(input.source ? { source: input.source } : {}),\n\t\tmessagePreview: input.message.slice(0, STEERING_MESSAGE_PREVIEW_LIMIT),\n\t\ttargets: input.targets.map((target) => ({\n\t\t\tindex: target.index,\n\t\t\tstate: target.state,\n\t\t\t...(target.reason ? { reason: target.reason } : {}),\n\t\t})),\n\t};\n\tstatus.requested++;\n\tstatus.lastRequestedAt = input.requestedAt;\n\tstatus.recent = [...status.recent, request].slice(-MAX_STEERING_REQUESTS);\n\tfor (const target of request.targets) incrementStateCount(status, target.state);\n\treturn request;\n}\n\nfunction incrementStateCount(status: SteeringStatus, state: SteeringTargetState): void {\n\tif (state === \"scheduled\") status.scheduled++;\n\telse if (state === \"routed\" || state === \"queued\") status.pending++;\n\telse if (state === \"delivered\" || state === \"late\") status.delivered++;\n\telse if (state === \"failed\") status.failed++;\n\telse if (state === \"recovered\") status.recovered++;\n}\n\nexport function updateSteeringTarget(\n\tstatus: SteeringStatus,\n\trequestId: string,\n\tindex: number,\n\tstate: SteeringTargetState,\n\tnow: number,\n\tfields: Pick<SteeringTargetStatus, \"reason\" | \"replacementRunId\"> = {},\n): SteeringTargetStatus | undefined {\n\tconst request = status.recent.find((candidate) => candidate.id === requestId);\n\tconst target = request?.targets.find((candidate) => candidate.index === index);\n\tif (!target) return undefined;\n\tif (state === \"late\" && target.state === \"recovered\") {\n\t\tif (target.lateDeliveredAt === undefined) {\n\t\t\ttarget.lateDeliveredAt = now;\n\t\t\tstatus.delivered++;\n\t\t\tstatus.lastDeliveredAt = now;\n\t\t}\n\t\tif (fields.reason) target.reason = fields.reason;\n\t\treturn target;\n\t}\n\tif (target.state === state) {\n\t\tif (state === \"routed\" && target.routedAt === undefined) target.routedAt = now;\n\t\tif (state === \"delivered\" && target.deliveredAt === undefined) target.deliveredAt = now;\n\t\tif (state === \"late\" && target.lateDeliveredAt === undefined) target.lateDeliveredAt = now;\n\t\tif (fields.reason) target.reason = fields.reason;\n\t\tif (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;\n\t\treturn target;\n\t}\n\tif ((target.state === \"routed\" || target.state === \"queued\") && state !== \"routed\" && state !== \"queued\")\n\t\tstatus.pending = Math.max(0, status.pending - 1);\n\ttarget.state = state;\n\tif (state === \"routed\") target.routedAt = now;\n\tif (state === \"delivered\") {\n\t\ttarget.deliveredAt = now;\n\t\tstatus.lastDeliveredAt = now;\n\t}\n\tif (state === \"late\") {\n\t\ttarget.lateDeliveredAt = now;\n\t\tstatus.lastDeliveredAt = now;\n\t}\n\tif (state === \"failed\") target.failedAt = now;\n\tif (state === \"recovered\") target.recoveredAt = now;\n\tif (fields.reason) target.reason = fields.reason;\n\tif (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;\n\tincrementStateCount(status, state);\n\treturn target;\n}\n\nexport function findSteeringRequest(status: SteeringStatus, requestId: string): SteeringRequestStatus | undefined {\n\treturn status.recent.find((request) => request.id === requestId);\n}\n\nexport function actionResultFromSteeringStatus(\n\tstatus: SteeringStatus,\n\tsourceRunId: string,\n\trequestId: string,\n\treplacementRunId?: string,\n): SteerActionResult | undefined {\n\tconst request = findSteeringRequest(status, requestId);\n\tif (!request) return undefined;\n\tconst targets = request.targets.map((target) => ({\n\t\tindex: target.index,\n\t\tstate: target.state,\n\t\t...(target.deliveredAt !== undefined ? { deliveredAt: target.deliveredAt } : {}),\n\t\t...(target.lateDeliveredAt !== undefined ? { lateDeliveredAt: target.lateDeliveredAt } : {}),\n\t\t...(target.reason ? { reason: target.reason } : {}),\n\t\t...(target.replacementRunId ? { replacementRunId: target.replacementRunId } : {}),\n\t}));\n\tconst states = targets.map((target) => target.state);\n\tlet state: SteerActionResult[\"state\"] = \"pending\";\n\tif (states.length > 0 && states.every((candidate) => candidate === \"delivered\")) state = \"delivered\";\n\telse if (states.length > 0 && states.every((candidate) => candidate === \"scheduled\")) state = \"scheduled\";\n\telse if (states.length > 0 && states.every((candidate) => candidate === \"recovered\")) state = \"recovered\";\n\telse if (states.length > 0 && states.every((candidate) => candidate === \"failed\" || candidate === \"late\"))\n\t\tstate = \"failed\";\n\telse if (\n\t\tstates.some((candidate) => candidate === \"failed\" || candidate === \"late\") &&\n\t\tstates.some((candidate) => candidate !== \"failed\" && candidate !== \"late\")\n\t)\n\t\tstate = \"partial\";\n\tconst effectiveReplacementRunId =\n\t\treplacementRunId ?? request.targets.find((target) => target.replacementRunId)?.replacementRunId;\n\tconst deliveryStatus =\n\t\tstates.length > 0 && states.every((candidate) => candidate === \"delivered\" || candidate === \"recovered\")\n\t\t\t? (\"delivered\" as const)\n\t\t\t: (\"queued\" as const);\n\treturn {\n\t\trequestId,\n\t\tstate,\n\t\tdeliveryStatus,\n\t\tsourceRunId,\n\t\t...(effectiveReplacementRunId ? { replacementRunId: effectiveReplacementRunId } : {}),\n\t\ttargets,\n\t};\n}\n\nexport function steeringActionIsTerminal(result: SteerActionResult | undefined): boolean {\n\treturn (\n\t\t((result?.targets.length ?? 0) > 0 &&\n\t\t\tresult?.targets.every((target) => target.state === \"queued\" || target.state === \"delivered\") === true) ||\n\t\tresult?.state === \"delivered\" ||\n\t\tresult?.state === \"scheduled\" ||\n\t\tresult?.state === \"partial\" ||\n\t\tresult?.state === \"recovered\" ||\n\t\tresult?.state === \"failed\"\n\t);\n}\n\nexport function terminalSteeringNoticeState(\n\tstatus: SteeringStatus,\n\trequestId: string,\n): \"failed\" | \"partial\" | undefined {\n\tconst request = status.recent.find((candidate) => candidate.id === requestId);\n\tif (\n\t\t!request ||\n\t\trequest.targets.some(\n\t\t\t(target) => target.state === \"routed\" || target.state === \"queued\" || target.state === \"scheduled\",\n\t\t)\n\t)\n\t\treturn undefined;\n\tconst hasSuccess = request.targets.some((target) => target.state === \"delivered\" || target.state === \"recovered\");\n\tconst hasFailure = request.targets.some((target) => target.state === \"failed\" || target.state === \"late\");\n\tif (hasSuccess && hasFailure) return \"partial\";\n\treturn hasFailure ? \"failed\" : undefined;\n}\n\nexport function claimSteeringRecovery(\n\tasyncDir: string,\n\tinput: { requestId: string; sourceRunId: string; committedAt: number },\n): { claimPath: string; markerPath: string } {\n\tconst recoveryDir = path.join(asyncDir, \"control\", \"steer-recovery\");\n\tfs.mkdirSync(recoveryDir, { recursive: true, mode: 0o700 });\n\tconst claimPath = path.join(recoveryDir, \"claim.json\");\n\tlet fd: number;\n\ttry {\n\t\tfd = fs.openSync(claimPath, \"wx\", 0o600);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"EEXIST\") {\n\t\t\tthrow new Error(\"Another steering recovery is already committed for this source run.\");\n\t\t}\n\t\tthrow error;\n\t}\n\tlet writeError: unknown;\n\ttry {\n\t\tfs.writeFileSync(fd, JSON.stringify({ version: 1, ...input }, null, 2), \"utf-8\");\n\t\tfs.fsyncSync(fd);\n\t} catch (error) {\n\t\twriteError = error;\n\t} finally {\n\t\tfs.closeSync(fd);\n\t}\n\tif (writeError !== undefined) {\n\t\tfs.rmSync(claimPath, { force: true });\n\t\tthrow writeError;\n\t}\n\tconst markerPath = path.join(recoveryDir, `${Buffer.from(input.requestId).toString(\"base64url\")}.json`);\n\ttry {\n\t\twritePrivateAtomicJson(markerPath, { version: 1, ...input });\n\t} catch (error) {\n\t\tfs.rmSync(claimPath, { force: true });\n\t\tthrow error;\n\t}\n\treturn { claimPath, markerPath };\n}\n\nexport function readSteeringStatus(asyncDir: string): SteeringStatus | undefined {\n\treturn readStatus(asyncDir)?.steering;\n}\n\nexport function remainingSteeringRecoveryLimits(\n\tdescriptor: Pick<SteeringRecoveryDescriptor, \"absoluteDeadlineAt\" | \"initialTurnBudget\" | \"initialToolBudget\">,\n\tstatus: Pick<AsyncStatus, \"turnBudget\" | \"turnCount\" | \"toolBudget\" | \"toolCount\">,\n\tnow = Date.now(),\n): {\n\ttimeoutMs?: number;\n\tabsoluteDeadlineAt?: number;\n\tturnBudget?: ResolvedTurnBudget;\n\ttoolBudget?: ResolvedToolBudget;\n} {\n\tconst limits: {\n\t\ttimeoutMs?: number;\n\t\tabsoluteDeadlineAt?: number;\n\t\tturnBudget?: ResolvedTurnBudget;\n\t\ttoolBudget?: ResolvedToolBudget;\n\t} = {};\n\tif (descriptor.absoluteDeadlineAt !== undefined) {\n\t\tconst timeoutMs = descriptor.absoluteDeadlineAt - now;\n\t\tif (timeoutMs <= 0) throw new Error(\"Source run has no remaining deadline budget; it remains paused.\");\n\t\tlimits.timeoutMs = timeoutMs;\n\t\tlimits.absoluteDeadlineAt = descriptor.absoluteDeadlineAt;\n\t}\n\tif (descriptor.initialTurnBudget) {\n\t\tconst consumed = status.turnBudget?.turnCount ?? status.turnCount ?? 0;\n\t\tconst totalRemaining = descriptor.initialTurnBudget.maxTurns + descriptor.initialTurnBudget.graceTurns - consumed;\n\t\tif (totalRemaining <= 0) throw new Error(\"Source run has no remaining turn budget; it remains paused.\");\n\t\tconst softRemaining = Math.max(0, descriptor.initialTurnBudget.maxTurns - consumed);\n\t\tlimits.turnBudget =\n\t\t\tsoftRemaining > 0\n\t\t\t\t? { maxTurns: softRemaining, graceTurns: totalRemaining - softRemaining }\n\t\t\t\t: { maxTurns: 1, graceTurns: totalRemaining - 1 };\n\t}\n\tif (descriptor.initialToolBudget) {\n\t\tconst consumed = status.toolBudget?.toolCount ?? status.toolCount ?? 0;\n\t\tconst hard = descriptor.initialToolBudget.hard - consumed;\n\t\tif (hard <= 0) throw new Error(\"Source run has no remaining tool budget; it remains paused.\");\n\t\tconst soft =\n\t\t\tdescriptor.initialToolBudget.soft === undefined ? undefined : descriptor.initialToolBudget.soft - consumed;\n\t\tlimits.toolBudget = {\n\t\t\thard,\n\t\t\t...(soft !== undefined && soft > 0 && soft < hard ? { soft } : {}),\n\t\t\tblock: descriptor.initialToolBudget.block,\n\t\t};\n\t}\n\treturn limits;\n}\n\nexport async function waitForSteeringAction(input: {\n\tasyncDir: string;\n\tsourceRunId: string;\n\trequestId: string;\n\ttimeoutMs: number;\n\tsignal?: AbortSignal;\n}): Promise<SteerActionResult | undefined> {\n\tconst deadline = Date.now() + input.timeoutMs;\n\twhile (Date.now() <= deadline) {\n\t\tif (input.signal?.aborted) return undefined;\n\t\tconst status = readSteeringStatus(input.asyncDir);\n\t\tconst result = status ? actionResultFromSteeringStatus(status, input.sourceRunId, input.requestId) : undefined;\n\t\tif (steeringActionIsTerminal(result)) return result;\n\t\tawait new Promise<void>((resolve) => setTimeout(resolve, Math.min(50, Math.max(1, deadline - Date.now()))));\n\t}\n\tconst status = readSteeringStatus(input.asyncDir);\n\treturn status ? actionResultFromSteeringStatus(status, input.sourceRunId, input.requestId) : undefined;\n}\n"]}