/** * InternalizationOrchestrator — Core-owned Skeleton (PRI-68) * * Consumes hydrated PITaskRecords, applies state-machine decisions, * acquires leases through RuntimeStateManager, and proposes successor * tasks — WITHOUT executing LLM calls or calling peer runners. * * Design: * - Single-step processing: wakeOnce() handles one task per call * - Host (plugin CLI or heartbeat trigger) decides when to call * - All task mutation goes through RuntimeStateManager (not direct store) * - Pure orchestration: no timers, no LLM calls, no peer runner imports * * @see docs/adr/0003-peer-agent-state-machine-orchestration.md */ import type { PDTaskStatus } from '../task-status.js'; import type { RuntimeStateManager } from '../store/runtime-state-manager.js'; import type { RunnerKind } from './peer-runner-contracts.js'; import type { DependencyGateResult, NextTaskProposal } from './internalization-state-machine.js'; export interface NoReadyTasksResult { decision: 'no_ready_tasks'; inspectedCount: number; /** Why no task could be leased: specific diagnosis */ reason: 'no_candidates' | 'filtered_out' | 'all_hydration_failed' | 'all_blocked' | 'all_dependency_failed' | 'all_lease_conflict' | 'all_retry_wait_pending'; } export interface BlockedResult { decision: 'blocked'; taskId: string; taskKind: RunnerKind; blockedBy: string[]; } export interface DependencyFailedResult { decision: 'dependency_failed'; taskId: string; taskKind: RunnerKind; failedDependencies: string[]; } export interface LeasedResult { decision: 'leased'; taskId: string; taskKind: RunnerKind; attemptCount: number; } export interface WouldLeaseResult { decision: 'would_lease'; taskId: string; taskKind: RunnerKind; gateResult: DependencyGateResult; } export interface LeaseConflictResult { decision: 'lease_conflict'; taskId: string; conflictReason: string; } export interface InvalidTaskMetadataResult { decision: 'invalid_task_metadata'; taskId: string; taskKind: string; } /** * Runtime decision labels for WakeOnceResult — used by host layers for * logging, metrics bucketing, and switch-statement exhaustiveness checks. * @experimental — consumed at runtime only; not used by the TypeScript * type system (discriminated union handles compile-time exhaustiveness). */ export declare const WAKE_ONCE_DECISIONS: readonly ['no_ready_tasks', 'blocked', 'dependency_failed', 'leased', 'would_lease', 'lease_conflict', 'invalid_task_metadata']; export type WakeOnceResult = NoReadyTasksResult | BlockedResult | DependencyFailedResult | LeasedResult | WouldLeaseResult | LeaseConflictResult | InvalidTaskMetadataResult; export interface ProposalCreatedResult { decision: 'proposal_created'; taskId: string; taskKind: RunnerKind; proposal: NextTaskProposal; } export type ProposeNextTaskResult = ProposalCreatedResult | null; export type CommitNextTaskResult = { decision: 'successor_created'; sourceTaskId: string; successorTaskId: string; successorKind: RunnerKind; } | { decision: 'successor_exists'; sourceTaskId: string; successorTaskId: string; successorKind: RunnerKind; } | { decision: 'no_successor'; sourceTaskId: string; reason: string; } | { decision: 'invalid_task_metadata'; taskId: string; reason: string; } | { decision: 'source_not_succeeded'; taskId: string; status: PDTaskStatus; } | { decision: 'task_not_found'; taskId: string; } /** INV-02: needs_revision — 不 seed 正常后继 (revision 由 runner 侧 repair/reopen 承担) */ | { decision: 'blocked_by_revision'; sourceTaskId: string; reason: string; runnerDecision: string; } /** INV-04: rejected — 终态拒绝, 无后继无 approval */ | { decision: 'blocked_by_rejection'; sourceTaskId: string; reason: string; runnerDecision: string; } /** artificer repair 任务完成 → 来源 evaluator 被 reopen 重跑修订轮 */ | { decision: 'revision_reopened'; sourceTaskId: string; reopenedTaskId: string; reason: string; } /** revision 波及下游: 已存在的 succeeded 后继被 reopen 重跑 (级联修订) */ | { decision: 'successor_reopened'; sourceTaskId: string; reopenedTaskId: string; successorKind: RunnerKind; } /** A/B: 同 causeId 重放的 no-op reopen — 已 materialized,不计数为恢复 */ | { decision: 'revision_reopen_noop'; sourceTaskId: string; reopenedTaskId: string; reason: string; } /** P0-3: 决策型任务缺少 durable + legacy verdict — fail-closed, 不 seed 任何后继 */ | { decision: 'blocked_missing_verdict'; taskId: string; reason: string; }; export interface InternalizationOrchestratorOptions { /** Lease owner identifier (injected by host) */ owner: string; /** Runtime kind for lease records */ runtimeKind: string; /** If true, evaluate but do NOT acquire lease (inspection / dry-run mode) */ dryRun?: boolean; } export interface InternalizationOrchestratorDeps { readonly stateManager: RuntimeStateManager; } export declare class InternalizationOrchestrator { private readonly owner; private readonly runtimeKind; private readonly dryRun; private readonly stateManager; constructor(deps: InternalizationOrchestratorDeps, options: InternalizationOrchestratorOptions); /** * Find the first leasable PI task, validate dependencies, and acquire lease * (or return a structured decision without mutating state). * * Algorithm: * 1. listTasks(pending) → filter PeerRunnerKind → hydrate * 2. If none, try listTasks(retry_wait) for recovery candidates * 3. For first valid PITaskRecord, resolve dependencyTaskIds via getTask * 4. validateInternalizationTaskReady → branch on gate result * 5. On proceed + dryRun → would_lease; on proceed + !dryRun → acquireLease * 6. On lease_conflict PDRuntimeError → structured LeaseConflictResult */ wakeOnce(taskKind?: RunnerKind): Promise; /** * Generate a successor task proposal for a succeeded task. * * Does NOT create the task — the caller decides whether to persist * the proposal via RuntimeStateManager.createTask(). * * Note: existingTasks is hardcoded to [] — the host layer is responsible * for deduplicating proposals against tasks already in the queue before * calling createTask(). * * Returns null if: * - Task not found * - Task not a valid PITaskRecord (hydration fails) * - Task status is not 'succeeded' * - No valid successor exists in the job graph */ proposeNextTask(taskId: string): Promise; /** * Commit a successor task proposal for a succeeded source task. * * Idempotent: if a matching successor already exists (same parentTaskId + * successorKind + channel), returns successor_exists without creating a duplicate. * * Steps: * 1. getTask(taskId) → null → task_not_found * 2. hydratePITaskRecord(task) → null → invalid_task_metadata * 3. task.status !== 'succeeded' → source_not_succeeded * 4. proposeNextTask(taskId) → null → no_successor * 5. Deduplicate: scan pending tasks for matching successor * 6. Found → successor_exists * 7. Not found → createTask + write PI metadata → successor_created */ commitNextTaskProposal(taskId: string): Promise; /** * P0-3 legacy 判据: 从该任务最近 succeeded run 的 output_payload **显式解析** * verdict (evaluation.decision / review.decision / rolloutDecision)。 * 这是修复前唯一持久 verdict 载体 — 对历史数据是真实证据而非猜测; * 新数据由 runner 的 durable runnerDecision 承载。解析失败/缺失 → undefined。 * rc-1/rc-2: output_payload 按不可信 JSON 处理,逐字段类型守卫。 */ /** * PRI-758: true when the evaluator's latest succeeded run carries a * deterministic adversarial replay that RAN and FAILED (passed===false). * This is the durable signal that the approved rule artifact was never * assembled and the chain must not ADVANCE while repair is required. */ private resolveAdversarialReplayFailed; private resolveLegacyRunnerVerdict; /** * Bounded reconciliation for the crash window between markTaskSucceeded * (durable, inside the runner) and commitNextTaskProposal (in-process, * called by the consumer AFTER run() returns). If the process dies between * them, the task stays succeeded forever and its outgoing transition * (successor seed / repair-source reopen / cascade reopen) is lost — * wakeOnce only scans pending/retry_wait. * * Strategy (bounded, not a blind sweep): * - scan only the N most recently updated succeeded tasks (default 10); * - only peer-runner kinds whose commit semantics are outgoing * transitions (rollout_reviewer included: its commit is a harmless * no-op verified by the verdict gate); * - arbitration ALWAYS goes through commitNextTaskProposal — the single * state-machine authority — whose paths are idempotent: * successor_exists / blocked_by_revision / blocked_by_rejection / * revision_reopened (same revisionCauseId → no-op) / * blocked_missing_verdict (fail-closed, surfaced not retried * aggressively — logged once per sweep). * - verdict semantics remain authoritative: needs_revision/rejected * never seed successors through this path. * * Restart-safe: calling this every consumer cycle is safe; duplicates * collapse into the idempotent commit results above. */ reconcileSucceededTransitions(options?: { /** 每周期处理条数上限 (bounded budget, 1..50) */ limit?: number; /** caller 持久化的扫描游标 (restart-durable);缺省从头开始 */ cursor?: { updatedAt: string; taskId: string; }; logger?: { info?: (msg: string) => void; }; }): Promise<{ scanned: number; recovered: number; alreadyMaterialized: number; blocked: number; outcomes: { taskId: string; decision: string; }[]; /** 本周期后的游标 — caller 必须持久化 (A3 restart 语义) */ nextCursor: { lastUpdatedAt: string; lastTaskId: string; }; /** true = 已扫到尾部,caller 应将游标重置回开头 (wrap-around) */ wrappedAround: boolean; }>; /** * Reopen a terminal (succeeded / needs_human_review) task for a revision round: * status → pending, attemptCount reset (revision is a new round, not a failure * retry), revisionCount++, optional feedback injected, optional artificer * dependency swap (evaluator rounds read the repair artificer's payload and * artifacts via the FIRST artificer dep — resolvePriorRepairIteration). * * Idempotent (INV-08): target already pending/retry_wait → no-op ok. * Restart-safe: all state is durable; double-reopen collapses to a no-op. */ reopenTaskForRevision(taskId: string, options?: { revisionFeedback?: string; replaceArtificerDependencyWith?: string; reason?: string; /** * P0-4 revision identity: 同一逻辑修订动作的稳定标识。相同 causeKey 对 * 已 reopen 目标重放 = 真正 no-op (不递增 revisionCount,不重写反馈); * 不同 causeKey = 新修订轮 (正常递增)。未提供时退化为旧行为 (每次 +1, * 仅建议内部测试使用;生产调用方必须传)。 */ revisionCauseId?: string; }): Promise<{ ok: boolean; reason: string; }>; /** * Find an existing successor task matching parentTaskId + successorKind + channel. * Scans pending tasks and hydrates to check PI metadata. * Returns the matching TaskRecord or null. */ private findExistingSuccessor; /** * Find candidate PI tasks by querying pending and retry_wait statuses. * Filters to only PeerRunnerKind taskKinds and hydrates to PITaskRecord. */ private findCandidates; /** * Resolve an array of dependency task IDs into TaskRecord instances. * Missing tasks (not yet created) are excluded from the result — this * causes validateInternalizationTaskReady to fail closed (treat as blocked). */ private resolveDependencies; } //# sourceMappingURL=internalization-orchestrator.d.ts.map