import type { RuntimeStateManager } from './store/runtime-state-manager.js'; import type { CandidateIntakeService } from './candidate-intake-service.js'; import type { LedgerAdapter } from './candidate-intake.js'; import type { RunnerResult, RunnerResultStatus } from './runner/runner-result.js'; import type { PDErrorCategory } from './error-categories.js'; import type { CandidateAdmissionResult, AdmissionDecision, PainProvenance } from './admission-gate.js'; import type { DiagnosticianOutputV1 } from './diagnostician-output.js'; import type { PainIngressV1Payload } from './pain-ingress-payload.js'; import type { PainCorrectionEvidence } from './context-payload.js'; export type { PainProvenance }; /** * Minimal interface for a diagnostician runner. * Both DiagnosticianRunner and SplitDiagnosticianRunner satisfy this. */ export interface DiagnosticianRunnerLike { run(taskId: string): Promise; } /** PRI-359: Increased from 4 to 8 to accommodate failed tool_calls evidence */ export declare const MAX_EVIDENCE_ENTRIES = 8; export declare const MAX_EVIDENCE_NOTE_CHARS = 200; /** * PRI-844: defensive upper bound for persisted Owner correction text. * This is a storage safety limit, NOT semantic truncation — the original * correction should remain complete when under the limit, and the whole * point of the field is that the Owner's own words survive verbatim. */ export declare const MAX_CORRECTION_EVIDENCE_TEXT_CHARS = 2000; /** SPEC-CODEX-PAIN-CLOSURE-001 CR-05 (§9.2): automatic post-processing continuation budget. */ export declare const MAX_POST_PROCESSING_ATTEMPTS = 3; /** * PRI-844: sanitize (strip PD tags / redact tokens / redact absolute paths) * without the 200-char generic evidence cap, then apply the defensive * storage bound. Deliberately NOT `sanitizeString` — its MAX_EVIDENCE_VALUE_CHARS * (200) head-truncation would destroy the verbatim correction this field exists * to preserve. */ export declare function boundCorrectionEvidenceText(correctionEvidence: PainCorrectionEvidence, workspaceDir?: string): PainCorrectionEvidence; export interface PainEvidenceEntry { sourceRef: string; note: string; } /** * PRI-640 (Host Attribution SPEC §6): which agent host a pain originated from. * Observability metadata only — MUST NOT participate in pain identity, * admission, diagnosis, approval, or activation. NULL at the persistence * layer means legacy / manual / unknown (read models report `unknown`). */ export type GovernanceHostKind = 'openclaw' | 'codex' | 'workbuddy'; export interface PainDetectedData { painId: string; painType: 'tool_failure' | 'subagent_error' | 'user_frustration'; source: string; reason: string; score?: number; sessionId?: string; agentId?: string; taskId?: string; traceId?: string; provenance?: PainProvenance; /** Codex Governance Closure SPEC §12: provenance `host_context_bound` names the host. */ hostKind?: GovernanceHostKind; evidence?: PainEvidenceEntry[]; /** * PRI-844: the Owner's verbatim correction, when this pain's context * contains one. Producers with no correction context leave it absent — * absence must never be replaced with fabricated correction text (rc-9). * Flows verbatim (defensively bounded) into diagnosticJson so the * diagnosis prompt sees the Owner's own words, not a trigger excerpt. */ correctionEvidence?: PainCorrectionEvidence; /** * PRI-642 SPEC §9: validated rev-2 ingress facts. When present, * buildDiagnosticJson writes them under the versioned `painIngress` * namespace next to the legacy top-level fields (one builder), and * re-entry validates the two against each other. */ painIngress?: PainIngressV1Payload; } export type PainSignalBridgeStatus = 'succeeded' | 'skipped' | 'failed' | 'retried' | 'degraded'; /** * PRI-539: A candidate that was admitted and ledgered but could not be * internalized (e.g. its mapped channel is MVP-disabled). Surfaced so the * Owner can see which candidates were dropped and why instead of silently * assuming every candidate was internalized (rc-9-no-silent-fallback / ERR-002). */ export interface NotInternalizableCandidate { candidateId: string; reason: string; } /** * PRI-642 SPEC §10: aggregate progress. `furthestStage` means AT LEAST ONE * item reached that stage — never that all candidates did; per-candidate * dispositions live in `candidateOutcomes`. */ export type PainFurthestStage = 'observed' | 'diagnosis_submitted' | 'diagnosis_completed' | 'candidate_processing' | 'internalization_seeded'; export interface PainProgressReport { furthestStage: PainFurthestStage; generatedCandidateIds: string[]; admittedCandidateIds: string[]; ledgerEntryIds: string[]; seededTaskIds: string[]; } /** PRI-642 SPEC §10: where each candidate stopped and why. */ export interface PainCandidateOutcome { candidateId: string; decision: 'admitted' | 'needs_evidence' | 'deferred'; ledgerEntryId?: string; seededTaskId?: string; reason: string; nextAction: string; /** * PRI-939 Option A — narrow Owner-visible projection of the auto-path reuse * evaluation outcome on a CREATE disposition. `reason` is present ONLY when * the evaluation was `unavailable` (bounded to 200 chars): that is the * "duplicate protection did not run" signal. A `recommended` outcome that * still created means the hook answered create/uncertain. Never persisted — * observation only (SPEC v0.3.2 §11), the Owner's verdict stays in * reuseEvidence[]. */ reuseRecommendation?: ReuseEvaluationOutcomeSignal; } /** * PRI-939 Option A — bounded projection of the reuse evaluation outcome for * Owner-facing result surfaces. Deliberately narrow (status + bounded reason): * the full recommendation payload is carried internally by the intake result * and surfaced by `pd candidate review`; results here only need to answer * "did the semantic gate run, and if not, why". */ export interface ReuseEvaluationOutcomeSignal { status: 'recommended' | 'unavailable'; reason?: string; } export interface PainSignalBridgeResult { status: PainSignalBridgeStatus; painId: string; taskId: string; runnerStatus?: RunnerResultStatus; runId?: string; artifactId?: string; candidateIds: string[]; ledgerEntryIds: string[]; admissionResults?: CandidateAdmissionResult[]; /** PRI-539: candidates admitted+ledgered but not internalizable (MVP-disabled channel). */ notInternalizable?: NotInternalizableCandidate[]; /** PRI-642 §10: aggregate progress (at-least-one semantics). */ progress?: PainProgressReport; /** PRI-642 §10: per-candidate disposition — the authority for mixed results. */ candidateOutcomes?: PainCandidateOutcome[]; /** * PRI-939 Option A — degradation/observability notes produced while shaping * this result (currently: semantic reuse evaluation unavailable on a CREATE * path). Merged into the CLI output by PainToPrincipleService; empty/absent * when everything ran clean. Observation only — never decision storage. */ observabilityWarnings?: string[]; errorCategory?: PDErrorCategory; message?: string; /** * PRI-638: recovery action surfaced by the runner. Only set when the failure * is an Owner capability decision (Diagnostician disabled), never for runtime * or provider faults — the two must remain distinguishable. */ nextAction?: string; } export interface PainSignalBridgeOptions { stateManager: RuntimeStateManager; runner: DiagnosticianRunnerLike; intakeService: CandidateIntakeService; ledgerAdapter: LedgerAdapter; owner?: string; autoIntakeEnabled?: boolean; /** Workspace directory — written into diagnosticJson so the diagnostician can locate files. */ workspaceDir?: string; /** * Pain Diagnosis Persistence: persist the diagnostician's root-cause * attribution into state.db pain_diagnoses (keyed by the canonical pain_id) * on diagnosis completion. Gated by the `pain_diagnosis_persistence` * feature flag — the factory resolves it from effectiveConfig; default * false keeps the pre-feature behavior (no writes). */ diagnosisPersistenceEnabled?: boolean; /** * PRI-720: seed NEW prompt/defer_archive chains with the explicit full-chain * topology override (pipelineMode='full_chain') instead of the channel-aware * short path. Resolved by the factory from the `prompt_full_pipeline` * feature flag (Owner switch: Console settings / .pd/config.yaml). Default * false = standard channel-aware topology. Affects only newly seeded chains. */ fullPipelinePromptSeeds?: boolean; eventEmitter?: { emitTelemetry: (event: { eventType: string; traceId: string; timestamp: string; payload: Record; }) => void; }; /** * PRI-624: resources the factory created for this bridge (its extra * SqliteConnection) that `dispose()` must release. Long-running workers * dispose bridges per cycle so file handles do not pin the workspace DB. */ ownedResources?: readonly { close: () => void | Promise; }[]; /** * PRI-638 P1-A: set ONLY by the factory's capability-disabled bridge. * While the Owner has switched the Diagnostician off, `onPainDetected` / * `executePendingDiagnosis` must ENSURE a durable task exists for new Pain * but must NEVER reset/re-trigger an existing task (that would erase * retry_wait/failed history and the retry budget). The runner is not * invoked, so provider calls stay 0. */ capabilityDisabled?: { readonly reason: string; readonly nextAction: string; }; } export declare function createDiagnosticianTaskId(painId: string): string; /** * Pain Diagnosis Persistence: link the diagnostician's root-cause attribution * to the canonical pain_id in state.db pain_diagnoses. Runs BEFORE admission * so the attribution is durable even when every candidate is later rejected — * the pain's diagnosis history must not depend on admission outcome. * Persistence is auxiliary to the admission→intake flow: failures degrade * observably via telemetry and never break the main pipeline * (rc-9-no-silent-fallback). * * PRI-935: module-level single authority. The bridge's onDiagnosisComplete * and the CLI diagnosis paths (`pd diagnose run`, `pd pain retry`) all write * through this function so the pain_diagnoses ledger cannot drift per-caller. * Callers own the `pain_diagnosis_persistence` flag gate — this function * persists whenever invoked. */ export declare function persistPainDiagnosis(deps: { stateManager: RuntimeStateManager; eventEmitter?: PainSignalBridgeOptions['eventEmitter']; }, opts: { painId: string; taskId: string; diagnosticianOutput: DiagnosticianOutputV1; artifactId: string | null; }): Promise; /** * SPEC-CODEX-PAIN-CLOSURE-001 CR-03 (§7.2): full-gate re-evaluation from * PERSISTED materials only (task diagnosticJson + candidate's source run), shared * by the CLI mutation paths so their write permission can never contradict * the production gate on the same material. The evaluation is PERSISTED as a * new admission version (append-only; idempotent by material fingerprint). */ export declare function evaluateAndPersistAdmissionFromStore(stateManager: Pick, candidateIds: readonly string[], input: { taskId: string; }): Promise<{ ok: true; decisions: Map; } | { ok: false; reason: string; nextAction: string; }>; export declare class PainSignalBridge { private readonly stateManager; private readonly runner; private readonly intakeService; private readonly ledgerAdapter; private readonly owner; private readonly autoIntakeEnabled; private readonly diagnosisPersistenceEnabled; private readonly fullPipelinePromptSeeds; private readonly workspaceDir; private readonly eventEmitter?; private readonly ownedResources; private readonly capabilityDisabled; constructor(opts: PainSignalBridgeOptions); /** * PRI-638 P1-A: the unified disabled outcome. `status: 'failed'` is kept * (existing RunnerResult vocabulary) but carries `capability_missing` + * `nextAction`, so no caller can mistake it for a provider/runtime fault. */ private disabledResult; /** * PRI-624: release every handle this bridge holds (state manager + the * factory-owned connection). Callers that keep the cached bridge (plugin * host process) may skip this; per-cycle workers MUST dispose to avoid * pinning the workspace SQLite files. */ /** * SPEC-CODEX-PAIN-CLOSURE-001 CR-05 (SPEC §9.1): continue the post-processing * of a SUCCEEDED diagnosis whose candidates never finished admission / * intake / seeding (crash between router `markTaskSucceeded` and this * bridge's post-processing, intake/seed exceptions, consumed-but-unseeded * orphans, pre-CR-03 rows with no persisted disposition). * * Hard guarantees: * - NEVER re-runs the diagnosis LLM (materials come from the persisted * run output + diagnosticJson); * - idempotent (Ledger writes dedup via existsForCandidate, seeds dedup * via findExistingDreamerTask, admission appends dedup by material * fingerprint); * - bounded (candidates whose processing state is terminal — complete / * awaiting_owner / exhausted — are skipped and reported, not rescanned); * - read/CLI paths never call this; only the worker continuation does. */ resumeSucceededDiagnosisPostProcessing(input: { taskId: string; }): Promise<{ status: 'resumed' | 'not_applicable' | 'blocked'; reason?: string; nextAction?: string; result?: PainSignalBridgeResult; processedCandidateIds?: string[]; skipped?: { candidateId: string; reason: string; }[]; remainingCount?: number; }>; dispose(): Promise; private emitAdmissionEvent; /** * Submit a pain signal without running diagnosis. * Creates the task as 'pending' for later execution by orchestrator wakeOnce/recovery-sweep. * Returns the taskId for progress tracking. * PRI-369: async pain-record CLI — fire-and-forget submission. */ submitPainSignal(data: PainDetectedData): Promise<{ taskId: string; }>; onPainDetected(data: PainDetectedData): Promise; /** * PRI-624 (Codex Closure Slice C): execute one already-submitted * Diagnostician task — the async counterpart of `submitPainSignal`. A * worker (Companion) or CLI uses this to advance a task that Slice B * admission enqueued without running an LLM in the hook. * * Unlike `onPainDetected` this NEVER resets task state: a worker retry * loop must preserve the retry budget (attemptCount/maxAttempts) exactly * like the peer-runner consumers do. The lease is acquired inside * `runner.run` via the existing Runtime V2 lease manager. * * Eligibility: 'pending', or 'retry_wait' whose backoff deadline * (lease_expires_at) has elapsed (isRetryWaitBackoffElapsed — the same * guard wakeOnce applies). 'leased' is skipped — the existing lease wins; * expired leases are the recovery sweep's job, not ours. 'succeeded' * returns the existing result (idempotent). 'failed' / * 'needs_human_review' are skipped: terminal states need explicit * Owner/manual action, never silent worker retries. */ executePendingDiagnosis(input: { taskId: string; /** * @deprecated PRI-642 SPEC §9: re-entry reads the provenance from the * persisted task payload; a caller-supplied value no longer overrides * it (no host-binding defaults, single authority). */ provenance?: PainProvenance; }): Promise; /** Fire-and-forget processing-domain update that must never break the loop (sync-safe). */ private safeUpdateAdmissionProcessing; /** * SPEC-CODEX-PAIN-CLOSURE-001 CR-05 (§9.2): bounded post-processing * failure bookkeeping in the independent processing domain. Default budget * is 3 automatic continuation attempts; exhaustion is a saved, readable * terminal state (manual_action_required), never a silent loop. The * diagnosis task's own attemptCount is never consumed by this. */ private recordPostProcessingFailure; /** * PRI-372 (T-G): Post-diagnosis processing extracted from onPainDetected(). * Handles admission → intake → seedDreamer after a successful diagnosis. * Reached only via onPainDetected / re-run above — DiagRouterRunner does * NOT call it (INF-9, PRI-857): the router only commits stage output. */ onDiagnosisComplete(opts: { taskId: string; diagnosticianOutput: DiagnosticianOutputV1 | undefined; painId: string; provenance: PainProvenance; inputEvidenceCount?: number; /** SPEC-CODEX-PAIN-CLOSURE-001 CR-02: the pain's correction evidence, when one was submitted. */ correctionEvidence?: PainCorrectionEvidence; }): Promise; private buildExistingResult; } //# sourceMappingURL=pain-signal-bridge.d.ts.map