create-principles-disciple 1.123.0 → 1.124.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/dist/runtime-v2/__tests__/pain-signal-bridge-execute-pending.test.d.ts +2 -0
- package/core/dist/runtime-v2/__tests__/pain-signal-bridge-execute-pending.test.d.ts.map +1 -0
- package/core/dist/runtime-v2/__tests__/pain-signal-bridge-execute-pending.test.js +106 -0
- package/core/dist/runtime-v2/__tests__/pain-signal-bridge-execute-pending.test.js.map +1 -0
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +2 -2
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/internalization-task-guards.d.ts +5 -8
- package/core/dist/runtime-v2/internalization/internalization-task-guards.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/internalization-task-guards.js +11 -12
- package/core/dist/runtime-v2/internalization/internalization-task-guards.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts +40 -0
- package/core/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-bridge.js +95 -0
- package/core/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts +10 -4
- package/core/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js +86 -51
- package/core/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +65 -4
- package/dist/installer.js.map +1 -1
- package/host-runtime/dist/governance-observation-store.d.ts +17 -0
- package/host-runtime/dist/governance-observation-store.js +38 -0
- package/host-runtime/dist/governance-signal-admission.js +55 -43
- package/host-runtime/dist/index.d.ts +3 -0
- package/host-runtime/dist/index.js +7 -0
- package/host-runtime/dist/internalization-consumer-cycle.d.ts +43 -0
- package/host-runtime/dist/internalization-consumer-cycle.js +468 -0
- package/host-runtime/dist/internalization-consumer-governance.d.ts +39 -0
- package/host-runtime/dist/internalization-consumer-governance.js +152 -0
- package/{plugin/dist/service/workspace-telemetry-sink.d.ts → host-runtime/dist/workspace-telemetry-emitter.d.ts} +5 -4
- package/host-runtime/dist/workspace-telemetry-emitter.js +74 -0
- package/install-layout/dist/index.d.ts +10 -0
- package/install-layout/dist/index.js +34 -1
- package/install-layout/dist/index.test.js +36 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/codex-ingest-catchup.d.ts +20 -0
- package/pd-cli/dist/commands/codex-ingest-catchup.d.ts.map +1 -0
- package/pd-cli/dist/commands/codex-ingest-catchup.js +95 -0
- package/pd-cli/dist/commands/codex-ingest-catchup.js.map +1 -0
- package/pd-cli/dist/commands/codex-worker.d.ts +20 -0
- package/pd-cli/dist/commands/codex-worker.d.ts.map +1 -0
- package/pd-cli/dist/commands/codex-worker.js +156 -0
- package/pd-cli/dist/commands/codex-worker.js.map +1 -0
- package/pd-cli/dist/index.js +47 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +432 -432
- package/plugin/dist/governance-audit.js +106 -106
- package/plugin/dist/rulehost-evidence.js +111 -111
- package/plugin/dist/service/internalization-auto-consumer-service.d.ts +11 -0
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/plugin/dist/service/auto-consumer-governance-wiring.d.ts +0 -34
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import fs from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
import Database from 'better-sqlite3';
|
|
37
|
-
import { collectSync, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, sanitizeString, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
|
|
37
|
+
import { collectSync, CORRECTION_SEED_KEYWORDS, buildToolFailureObservation, evaluateTriage, evaluateTriggerController, resolveSourceKind, PainToPrincipleService, PrincipleTreeLedgerAdapter, RuntimeStateManager, createDiagnosticianTaskId, disposePainSignalBridgesForWorkspace, sanitizeString, MAX_EVIDENCE_VALUE_CHARS, } from '@principles/core/runtime-v2';
|
|
38
38
|
import { deriveProductionCorrectionPainIdentity, deriveProductionToolPainIdentity, hasProductionPainSchema, PRODUCTION_WRITE_TOOLS, } from './production-pain-evidence.js';
|
|
39
39
|
import { promoteGovernanceEvidence, ensureGovernanceSchema } from './governance-observation-store.js';
|
|
40
40
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
@@ -632,51 +632,63 @@ export async function ensureGovernanceDiagnosticianTask(input) {
|
|
|
632
632
|
const taskId = createDiagnosticianTaskId(input.canonicalPainId);
|
|
633
633
|
// Cross-store crash recovery (SPEC §13): a task may already exist from a
|
|
634
634
|
// crashed prior attempt (crash-after-create, before the link write).
|
|
635
|
+
// PRI-624: the state manager MUST be closed on every exit — the Slice C
|
|
636
|
+
// worker calls this seam every cycle, and a leaked handle would pin the
|
|
637
|
+
// workspace state.db for the life of the worker process.
|
|
635
638
|
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
639
|
+
try {
|
|
640
|
+
await stateManager.initialize();
|
|
641
|
+
const existing = await stateManager.getTask(taskId);
|
|
642
|
+
if (existing === null) {
|
|
643
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
644
|
+
const config = loadPdConfigForPlugin(workspaceDir);
|
|
645
|
+
if (!config.ok) {
|
|
646
|
+
return { ok: false, reason: `pd_config_invalid:${config.errors[0]?.reason ?? 'unknown'}`, nextAction: config.errors[0]?.nextAction ?? 'Repair .pd/config.yaml and run reconciliation.' };
|
|
647
|
+
}
|
|
648
|
+
const service = new PainToPrincipleService({
|
|
649
|
+
workspaceDir,
|
|
650
|
+
stateDir,
|
|
651
|
+
ledgerAdapter: new PrincipleTreeLedgerAdapter({ stateDir }),
|
|
652
|
+
owner: 'codex-governance',
|
|
653
|
+
asyncMode: true,
|
|
654
|
+
effectiveConfig: config.effective,
|
|
655
|
+
getEnvVar: (name) => process.env[name],
|
|
656
|
+
});
|
|
657
|
+
const result = await service.recordPain({
|
|
658
|
+
painId: input.canonicalPainId,
|
|
659
|
+
painType: payloadParsed.painType,
|
|
660
|
+
source: payloadParsed.source,
|
|
661
|
+
reason: payloadParsed.reason,
|
|
662
|
+
score: payloadParsed.score,
|
|
663
|
+
sessionId: payloadParsed.sessionId,
|
|
664
|
+
provenance: 'host_context_bound',
|
|
665
|
+
hostKind: 'codex',
|
|
666
|
+
evidence: [...payloadParsed.evidence],
|
|
667
|
+
recordObservability: true,
|
|
668
|
+
});
|
|
669
|
+
// recordPain (async) leaves a cached bridge holding open SQLite
|
|
670
|
+
// handles; this seam runs per reconciliation cycle under the Slice C
|
|
671
|
+
// worker, so release it immediately (hook processes exit anyway).
|
|
672
|
+
await disposePainSignalBridgesForWorkspace(workspaceDir).catch(() => undefined);
|
|
673
|
+
if (result.status === 'failed') {
|
|
674
|
+
return { ok: false, reason: `task_submit_failed:${(result.message ?? result.failureCategory ?? 'unknown').slice(0, 160)}`, nextAction: 'inspect the diagnostician runtime profile; reconciliation retries task creation without losing the admitted pain' };
|
|
675
|
+
}
|
|
667
676
|
}
|
|
677
|
+
db.prepare('UPDATE governance_signal_admissions SET diagnostician_task_id = ?, updated_at = ? WHERE logical_observation_key = ?')
|
|
678
|
+
.run(taskId, new Date().toISOString(), input.logicalObservationKey);
|
|
679
|
+
return {
|
|
680
|
+
ok: true,
|
|
681
|
+
taskId,
|
|
682
|
+
created: existing === null,
|
|
683
|
+
duplicate: existing !== null,
|
|
684
|
+
// The link was just written because the task already existed → a real
|
|
685
|
+
// Case B repair (not a no-op on an already-linked marker).
|
|
686
|
+
linkRepaired: existing !== null,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
await stateManager.close().catch(() => undefined);
|
|
668
691
|
}
|
|
669
|
-
db.prepare('UPDATE governance_signal_admissions SET diagnostician_task_id = ?, updated_at = ? WHERE logical_observation_key = ?')
|
|
670
|
-
.run(taskId, new Date().toISOString(), input.logicalObservationKey);
|
|
671
|
-
return {
|
|
672
|
-
ok: true,
|
|
673
|
-
taskId,
|
|
674
|
-
created: existing === null,
|
|
675
|
-
duplicate: existing !== null,
|
|
676
|
-
// The link was just written because the task already existed → a real
|
|
677
|
-
// Case B repair (not a no-op on an already-linked marker).
|
|
678
|
-
linkRepaired: existing !== null,
|
|
679
|
-
};
|
|
680
692
|
}
|
|
681
693
|
catch (error) {
|
|
682
694
|
const detail = error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200);
|
|
@@ -11,6 +11,9 @@ export * from './production-pain-evidence.js';
|
|
|
11
11
|
export * from './governance-observation-store.js';
|
|
12
12
|
export * from './governance-signal-admission.js';
|
|
13
13
|
export * from './host-liveness-contract.js';
|
|
14
|
+
export * from './internalization-consumer-governance.js';
|
|
15
|
+
export * from './internalization-consumer-cycle.js';
|
|
16
|
+
export * from './workspace-telemetry-emitter.js';
|
|
14
17
|
export * from './product-telemetry/consent-store.js';
|
|
15
18
|
export * from './product-telemetry/eligibility.js';
|
|
16
19
|
export * from './product-telemetry/exporter.js';
|
|
@@ -10,6 +10,13 @@ export * from './production-pain-evidence.js';
|
|
|
10
10
|
export * from './governance-observation-store.js';
|
|
11
11
|
export * from './governance-signal-admission.js';
|
|
12
12
|
export * from './host-liveness-contract.js';
|
|
13
|
+
// PRI-624 Slice C: shared internalization consumer execution (OpenClaw
|
|
14
|
+
// auto-consumer + Companion workspace worker call the same cycle).
|
|
15
|
+
export * from './internalization-consumer-governance.js';
|
|
16
|
+
export * from './internalization-consumer-cycle.js';
|
|
17
|
+
// PRI-634 A3: workspace-scoped critical telemetry — canonical implementation
|
|
18
|
+
// moved from openclaw-plugin so both hosts share ONE semantics.
|
|
19
|
+
export * from './workspace-telemetry-emitter.js';
|
|
13
20
|
// Anonymous Product Telemetry v1 (PRI-595~603) — opt-in, default-off,
|
|
14
21
|
// read-only with respect to all PD governance facts.
|
|
15
22
|
export * from './product-telemetry/consent-store.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ConsumerGovernanceLogger } from './internalization-consumer-governance.js';
|
|
2
|
+
export declare const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = "internalization_auto_consumer";
|
|
3
|
+
/** Structural logger port — OpenClaw PluginLogger and worker loggers satisfy this. */
|
|
4
|
+
export interface ConsumerCycleLogger extends ConsumerGovernanceLogger {
|
|
5
|
+
info: (msg: string) => void;
|
|
6
|
+
warn: (msg: string) => void;
|
|
7
|
+
error: (msg: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface ConsumerHostToolCatalog {
|
|
10
|
+
readonly readOnlyTools: readonly string[];
|
|
11
|
+
readonly writeTools: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface InternalizationConsumerCyclePorts {
|
|
14
|
+
/** Runner-owner label recorded on leases ('auto-consumer' | 'companion-worker' | …). */
|
|
15
|
+
readonly owner: string;
|
|
16
|
+
readonly logger: ConsumerCycleLogger;
|
|
17
|
+
/** Structured event sink (OpenClaw: SystemLogger.log; worker: its own log). */
|
|
18
|
+
readonly emitEvent: (event: string, payloadJson: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Host-declared runtime-authoritative tool catalog (evaluator; PRI-630).
|
|
21
|
+
* Optional: hosts that have not declared a catalog (Codex as of PRI-624)
|
|
22
|
+
* omit it and the evaluator keeps its pre-catalog behavior — a wrong-host
|
|
23
|
+
* catalog would be worse than none.
|
|
24
|
+
*/
|
|
25
|
+
readonly hostToolCatalog?: ConsumerHostToolCatalog;
|
|
26
|
+
/** Log label used in human log prefixes ('AutoConsumer' | 'CodexWorker' | …). */
|
|
27
|
+
readonly logLabel: string;
|
|
28
|
+
readonly envGetter?: (name: string) => string | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface InternalizationConsumerCycleOutcome {
|
|
31
|
+
readonly ran: boolean;
|
|
32
|
+
readonly skipReason?: string;
|
|
33
|
+
readonly taskId?: string;
|
|
34
|
+
readonly taskKind?: string;
|
|
35
|
+
readonly runStatus?: 'succeeded' | 'failed' | 'retried';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run ONE bounded consumer cycle for a workspace. At most one downstream task
|
|
39
|
+
* is leased and executed per cycle (DEFAULT_CONSUMER_MAX_TASKS_PER_CYCLE=1);
|
|
40
|
+
* backlog converges over repeated cycles and never starves reconciliation
|
|
41
|
+
* (bounded budget in the finally block).
|
|
42
|
+
*/
|
|
43
|
+
export declare function runInternalizationConsumerCycle(workspaceDir: string, ports: InternalizationConsumerCyclePorts): Promise<InternalizationConsumerCycleOutcome>;
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared host-neutral internalization consumer cycle (PRI-624 Slice C).
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from openclaw-plugin's
|
|
5
|
+
* `internalization-auto-consumer-service.ts` `runConsumerCycle` so that the
|
|
6
|
+
* OpenClaw auto-consumer scheduler and the Companion workspace worker call
|
|
7
|
+
* ONE downstream execution implementation (SPEC §13: "reusing existing
|
|
8
|
+
* downstream consumer logic" — the Companion must not copy it):
|
|
9
|
+
*
|
|
10
|
+
* flag gate → config/runtime config → state handle → orchestrator(dryRun)
|
|
11
|
+
* → queue read model → consumer decision → wakeOnce loop → adapter
|
|
12
|
+
* → runner construction (dreamer…rollout_reviewer) → runner.run
|
|
13
|
+
* → commit proposal → finally: recovery sweep + reconciliation budget
|
|
14
|
+
*
|
|
15
|
+
* The lease is acquired INSIDE runner.run by the Runtime V2 lease manager
|
|
16
|
+
* (persisted in `<workspace>/.pd/state.db`, cross-process safe) — this cycle
|
|
17
|
+
* never leases by itself, so two schedulers (OpenClaw + Companion) racing on
|
|
18
|
+
* one workspace converge to exactly-once execution via lease_conflict.
|
|
19
|
+
*
|
|
20
|
+
* Hosts inject: a logger, a structured-event sink, their tool catalog, and an
|
|
21
|
+
* env getter. Everything else is the existing production path.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE, } from '@principles/core/runtime-v2';
|
|
25
|
+
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
26
|
+
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from './pd-config.js';
|
|
27
|
+
import { createEvaluatorRepairDeps, createRolloutGovernanceDeps } from './internalization-consumer-governance.js';
|
|
28
|
+
import { WorkspaceTelemetryEmitter } from './workspace-telemetry-emitter.js';
|
|
29
|
+
export const INTERNALIZATION_AUTO_CONSUMER_FLAG_ID = 'internalization_auto_consumer';
|
|
30
|
+
function contentHashFn(text) {
|
|
31
|
+
return createHash('sha256').update(text).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
function formatRunOnceCommand(workspaceDir) {
|
|
34
|
+
return `pd runtime internalization run-once --workspace "${workspaceDir}" --runner dreamer --runtime config --json`;
|
|
35
|
+
}
|
|
36
|
+
function getNextActionForError(category) {
|
|
37
|
+
if (category === 'lease_conflict') {
|
|
38
|
+
return 'Retry later or check for concurrent worker processes.';
|
|
39
|
+
}
|
|
40
|
+
if (category === 'timeout') {
|
|
41
|
+
return 'Check model provider service status/latency, or increase timeout settings in workflows.yaml.';
|
|
42
|
+
}
|
|
43
|
+
if (category === 'cancelled') {
|
|
44
|
+
return 'Re-enqueue or restart the task if it was cancelled by mistake.';
|
|
45
|
+
}
|
|
46
|
+
if (category === 'output_invalid') {
|
|
47
|
+
return 'Verify if model outputs conform to expected schema and adjust prompt or validation templates if needed.';
|
|
48
|
+
}
|
|
49
|
+
if (category === 'input_invalid') {
|
|
50
|
+
return 'Check predecessor task outputs and database integrity for malformed input references.';
|
|
51
|
+
}
|
|
52
|
+
if (category === 'max_attempts_exceeded') {
|
|
53
|
+
return 'Investigate persistent failures, correct the root issue, and clear last_error or reset attempt count.';
|
|
54
|
+
}
|
|
55
|
+
return 'Run: pd runtime internalization run-once --runner dreamer --runtime config --json to isolate the failure.';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* PRI-554: safe per-cycle expired-lease recovery sweep.
|
|
59
|
+
* - sweep 失败不阻塞 consumer cycle (下轮再试); 显式留痕
|
|
60
|
+
* INTERNALIZATION_CONSUMER_RECOVER_FAILED (rc-9)。
|
|
61
|
+
*/
|
|
62
|
+
async function safeRunRecoverySweep(stateManager, ports) {
|
|
63
|
+
const { logger, emitEvent, logLabel } = ports;
|
|
64
|
+
try {
|
|
65
|
+
const sweep = await stateManager.runRecoverySweep();
|
|
66
|
+
if (sweep.recovered > 0 || sweep.errors.length > 0) {
|
|
67
|
+
emitEvent('INTERNALIZATION_CONSUMER_RECOVERED', JSON.stringify({
|
|
68
|
+
recovered: sweep.recovered,
|
|
69
|
+
failed: sweep.errors.length,
|
|
70
|
+
errors: sweep.errors,
|
|
71
|
+
}));
|
|
72
|
+
logger.info(`[PD:${logLabel}] Recovery sweep: recovered=${sweep.recovered} failed=${sweep.errors.length}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (sweepErr) {
|
|
76
|
+
emitEvent('INTERNALIZATION_CONSUMER_RECOVER_FAILED', String(sweepErr));
|
|
77
|
+
logger.warn(`[PD:${logLabel}] Recovery sweep failed: ${String(sweepErr)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A (crash window / 最终复核): bounded + fair + restart-durable 的
|
|
82
|
+
* succeeded-transition reconciliation 预算。
|
|
83
|
+
* - ASC 稳定全序 (SQL ORDER BY updated_at, task_id) + 独占元组游标;
|
|
84
|
+
* - 每周期 RECONCILIATION_BUDGET 条,扫到尾部 wrap-around 重置;
|
|
85
|
+
* - 游标持久化于 state.db reconciliation_cursor — restart 后继续;
|
|
86
|
+
* - P1: 任何 reconcile_error 显式留痕 (不依赖 recovered>0)。
|
|
87
|
+
*/
|
|
88
|
+
const RECONCILIATION_BUDGET = 5;
|
|
89
|
+
async function runReconciliationBudget(workspaceDir, orchestrator, ports) {
|
|
90
|
+
const { logger, emitEvent, logLabel } = ports;
|
|
91
|
+
try {
|
|
92
|
+
const conn = new SqliteConnection(workspaceDir);
|
|
93
|
+
try {
|
|
94
|
+
const cursorStore = new SqliteReconciliationCursorStore(conn);
|
|
95
|
+
const stored = cursorStore.get(SUCCEEDED_TRANSITIONS_SCOPE);
|
|
96
|
+
const recon = await orchestrator.reconcileSucceededTransitions({
|
|
97
|
+
limit: RECONCILIATION_BUDGET,
|
|
98
|
+
cursor: stored ? { updatedAt: stored.lastUpdatedAt, taskId: stored.lastTaskId } : undefined,
|
|
99
|
+
logger: { info: (msg) => logger.info(msg) },
|
|
100
|
+
});
|
|
101
|
+
if (recon.wrappedAround) {
|
|
102
|
+
cursorStore.clear(SUCCEEDED_TRANSITIONS_SCOPE);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
cursorStore.set(SUCCEEDED_TRANSITIONS_SCOPE, recon.nextCursor);
|
|
106
|
+
}
|
|
107
|
+
const errors = recon.outcomes.filter((o) => o.decision.startsWith('reconcile_error'));
|
|
108
|
+
if (recon.recovered > 0 || errors.length > 0) {
|
|
109
|
+
emitEvent('INTERNALIZATION_CONSUMER_RECONCILED', JSON.stringify({
|
|
110
|
+
scanned: recon.scanned,
|
|
111
|
+
recovered: recon.recovered,
|
|
112
|
+
alreadyMaterialized: recon.alreadyMaterialized,
|
|
113
|
+
blocked: recon.blocked,
|
|
114
|
+
wrappedAround: recon.wrappedAround,
|
|
115
|
+
recoveries: recon.outcomes.filter((o) => o.decision === 'successor_created' || o.decision.includes('reopened')),
|
|
116
|
+
// P1 (最终复核): per-task 错误显式可观测 — 即使 recovered=0
|
|
117
|
+
errors,
|
|
118
|
+
}));
|
|
119
|
+
logger.info(`[PD:${logLabel}] Reconciliation: recovered=${recon.recovered} errors=${errors.length} wrapped=${recon.wrappedAround}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
try {
|
|
124
|
+
conn.close();
|
|
125
|
+
}
|
|
126
|
+
catch { /* best-effort */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (reconErr) {
|
|
130
|
+
// reconciliation 失败不阻塞周期 (下轮再试);显式留痕 (rc-9)
|
|
131
|
+
emitEvent('INTERNALIZATION_CONSUMER_RECONCILE_FAILED', String(reconErr));
|
|
132
|
+
logger.warn(`[PD:${logLabel}] Succeeded-transition reconciliation failed: ${String(reconErr)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run ONE bounded consumer cycle for a workspace. At most one downstream task
|
|
137
|
+
* is leased and executed per cycle (DEFAULT_CONSUMER_MAX_TASKS_PER_CYCLE=1);
|
|
138
|
+
* backlog converges over repeated cycles and never starves reconciliation
|
|
139
|
+
* (bounded budget in the finally block).
|
|
140
|
+
*/
|
|
141
|
+
export async function runInternalizationConsumerCycle(workspaceDir, ports) {
|
|
142
|
+
const { logger, emitEvent, logLabel, owner } = ports;
|
|
143
|
+
const { hostToolCatalog } = ports;
|
|
144
|
+
const envGetter = ports.envGetter ?? ((name) => process.env[name]);
|
|
145
|
+
let orchestrator = null;
|
|
146
|
+
const flag = loadFeatureFlagFromConfig(workspaceDir, INTERNALIZATION_AUTO_CONSUMER_FLAG_ID, {
|
|
147
|
+
info: (msg) => logger.info(msg),
|
|
148
|
+
warn: (msg) => logger.warn(msg),
|
|
149
|
+
});
|
|
150
|
+
if (!flag.enabled) {
|
|
151
|
+
const disabledInfo = JSON.stringify({
|
|
152
|
+
reason: 'internalization_auto_consumer_disabled',
|
|
153
|
+
nextAction: formatRunOnceCommand(workspaceDir),
|
|
154
|
+
flagSource: flag.source,
|
|
155
|
+
});
|
|
156
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', disabledInfo);
|
|
157
|
+
logger.info(`[PD:${logLabel}] Cycle skipped: auto-consumer disabled. Source: ${flag.source}`);
|
|
158
|
+
return { ran: false, skipReason: 'internalization_auto_consumer_disabled' };
|
|
159
|
+
}
|
|
160
|
+
const configResult = loadPdConfigForPlugin(workspaceDir);
|
|
161
|
+
if (!configResult.ok) {
|
|
162
|
+
const malformedInfo = JSON.stringify({
|
|
163
|
+
reason: 'config_malformed',
|
|
164
|
+
nextAction: configResult.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry',
|
|
165
|
+
errors: configResult.errors.map((e) => e.reason),
|
|
166
|
+
});
|
|
167
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', malformedInfo);
|
|
168
|
+
logger.warn(`[PD:${logLabel}] Config malformed, skipping cycle.`);
|
|
169
|
+
return { ran: false, skipReason: 'config_malformed' };
|
|
170
|
+
}
|
|
171
|
+
const runtimeConfigResult = resolveRuntimeConfigFromPdConfig(configResult.effective, (name) => envGetter(name));
|
|
172
|
+
if (isRuntimeConfigError(runtimeConfigResult)) {
|
|
173
|
+
const rtInfo = JSON.stringify({
|
|
174
|
+
reason: 'runtime_config_error',
|
|
175
|
+
message: runtimeConfigResult.message,
|
|
176
|
+
nextAction: runtimeConfigResult.nextAction,
|
|
177
|
+
});
|
|
178
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', rtInfo);
|
|
179
|
+
logger.warn(`[PD:${logLabel}] Runtime config error: ${runtimeConfigResult.message}`);
|
|
180
|
+
return { ran: false, skipReason: 'runtime_config_error' };
|
|
181
|
+
}
|
|
182
|
+
let handle = null;
|
|
183
|
+
try {
|
|
184
|
+
handle = await createRuntimeStateHandle({ workspaceDir, readonly: false });
|
|
185
|
+
const { stateManager } = handle;
|
|
186
|
+
// A (最终复核): orchestrator 必须在所有队列状态相关的早退之前创建 —
|
|
187
|
+
// 纯 orphan 场景 (succeeded 后 crash、队列全空 → readyTaskCount=0) 恰恰
|
|
188
|
+
// 是 reconciliation 存在的理由; 若 orchestrator 为 null, finally 的
|
|
189
|
+
// bounded budget 不会执行, orphan 永远无法恢复。
|
|
190
|
+
const { runtimeKind } = runtimeConfigResult;
|
|
191
|
+
orchestrator = new InternalizationOrchestrator({ stateManager }, { owner, runtimeKind, dryRun: true });
|
|
192
|
+
const readModel = new InternalizationQueueReadModel(stateManager);
|
|
193
|
+
readModel.setPolicy({
|
|
194
|
+
enabledChannels: new Set(['prompt', 'code_tool_hook', 'defer_archive']),
|
|
195
|
+
actionableTaskKinds: new Set(MVP_CORE_TASK_KINDS),
|
|
196
|
+
});
|
|
197
|
+
const snapshot = await readModel.getSnapshot();
|
|
198
|
+
if (snapshot.readyTasks.length > 5) {
|
|
199
|
+
logger.warn(`[PD:${logLabel}] Backlog detected: ${snapshot.readyTasks.length} tasks ready. Processing only one task.`);
|
|
200
|
+
}
|
|
201
|
+
// PRI-419 amendment: when internalization_full_chain is ON (default), the
|
|
202
|
+
// consumer advances the full dreamer→…→evaluator→rollout_reviewer chain
|
|
203
|
+
// so artifacts reach validation_status='validated' and the approval queue is
|
|
204
|
+
// populated unattended. The human gate is the approval queue (Console), not
|
|
205
|
+
// the rollout_reviewer step. flag-off reverts to dreamer-only.
|
|
206
|
+
const fullChainFlag = loadFeatureFlagFromConfig(workspaceDir, 'internalization_full_chain');
|
|
207
|
+
const consumerRunnerKinds = fullChainFlag.enabled
|
|
208
|
+
? FULL_CHAIN_CONSUMER_RUNNER_KINDS
|
|
209
|
+
: DEFAULT_CONSUMER_RUNNER_KINDS;
|
|
210
|
+
const decision = computeConsumerDecision({
|
|
211
|
+
autoConsumerEnabled: true,
|
|
212
|
+
readyTaskCount: snapshot.readyTasks.length,
|
|
213
|
+
runnerKinds: consumerRunnerKinds,
|
|
214
|
+
});
|
|
215
|
+
if (!decision.shouldConsume) {
|
|
216
|
+
// 早退不跳过 reconciliation — finally 的 bounded budget 仍会执行
|
|
217
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({
|
|
218
|
+
reason: decision.reason,
|
|
219
|
+
readyTaskCount: snapshot.readyTasks.length,
|
|
220
|
+
}));
|
|
221
|
+
return { ran: false, skipReason: decision.reason };
|
|
222
|
+
}
|
|
223
|
+
// Advance the configured runner kinds in priority order (dreamer first,
|
|
224
|
+
// then philosopher→…→rollout_reviewer under full-chain scope). Lease the
|
|
225
|
+
// first ready task whose dependencies are satisfied.
|
|
226
|
+
let wakeResult = null;
|
|
227
|
+
let lastSkipDecision = 'no_ready_tasks';
|
|
228
|
+
let lastSkipReason;
|
|
229
|
+
for (const kind of decision.runnerKinds) {
|
|
230
|
+
const candidate = await orchestrator.wakeOnce(kind);
|
|
231
|
+
if (candidate.decision === 'would_lease') {
|
|
232
|
+
wakeResult = candidate;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
// Keep decision/reason paired across iterations so the final SKIP log
|
|
236
|
+
// never reports a stale reason from an earlier kind (EP-03 observability).
|
|
237
|
+
lastSkipDecision = candidate.decision;
|
|
238
|
+
lastSkipReason = candidate.decision === 'no_ready_tasks' ? candidate.reason : undefined;
|
|
239
|
+
}
|
|
240
|
+
if (!wakeResult) {
|
|
241
|
+
// A: 预算由周期 finally 统一执行 (每周期恰一次,含 backlog 场景)
|
|
242
|
+
const skipPayload = { decision: lastSkipDecision };
|
|
243
|
+
if (lastSkipReason) {
|
|
244
|
+
skipPayload.reason = lastSkipReason;
|
|
245
|
+
}
|
|
246
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify(skipPayload));
|
|
247
|
+
logger.info(`[PD:${logLabel}] No task to consume: ${lastSkipDecision}`);
|
|
248
|
+
return { ran: false, skipReason: lastSkipDecision };
|
|
249
|
+
}
|
|
250
|
+
let adapter;
|
|
251
|
+
if (runtimeKind === 'pi-ai') {
|
|
252
|
+
// PRI-419: when l2_dreamer flag is on AND this is a dreamer task, route
|
|
253
|
+
// through the L2 multi-turn agent loop. Non-dreamer runners always use PiAi.
|
|
254
|
+
const l2Flag = loadFeatureFlagFromConfig(workspaceDir, 'l2_dreamer');
|
|
255
|
+
if (l2Flag.enabled && wakeResult.taskKind === 'dreamer') {
|
|
256
|
+
const stateDir = `${workspaceDir}/.state`;
|
|
257
|
+
const principleReader = buildL2PrincipleReaderFromLedger(loadLedger(stateDir), {
|
|
258
|
+
logger: { warn: (msg) => logger.warn(msg) },
|
|
259
|
+
});
|
|
260
|
+
adapter = new L2AgentLoopAdapter({
|
|
261
|
+
provider: runtimeConfigResult.provider ?? 'openai',
|
|
262
|
+
model: runtimeConfigResult.model ?? 'gpt-4o',
|
|
263
|
+
apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
|
|
264
|
+
baseUrl: runtimeConfigResult.baseUrl,
|
|
265
|
+
workspace: workspaceDir,
|
|
266
|
+
totalBudgetMs: runtimeConfigResult.timeoutMs,
|
|
267
|
+
}, {
|
|
268
|
+
artifactReader: {
|
|
269
|
+
// Explicit adapter: PIArtifactRecord → PdL2ArtifactReader. The store returns
|
|
270
|
+
// PIArtifactRecord (with PIArtifactKind enum); map to the ArtifactSummary shape.
|
|
271
|
+
getArtifactById: async (id) => {
|
|
272
|
+
const r = await stateManager.piArtifactStore.getArtifactById(id);
|
|
273
|
+
return r ? { artifactId: r.artifactId, artifactKind: String(r.artifactKind), sourceTaskId: r.sourceTaskId, contentJson: r.contentJson, createdAt: r.createdAt } : null;
|
|
274
|
+
},
|
|
275
|
+
listBySourceTaskId: async (taskId) => {
|
|
276
|
+
const records = await stateManager.piArtifactStore.listBySourceTaskId(taskId);
|
|
277
|
+
return records.map(r => ({ artifactId: r.artifactId, artifactKind: String(r.artifactKind), sourceTaskId: r.sourceTaskId, contentJson: r.contentJson, createdAt: r.createdAt }));
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
principleReader,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
adapter = new PiAiRuntimeAdapter({
|
|
285
|
+
provider: runtimeConfigResult.provider ?? 'openai',
|
|
286
|
+
model: runtimeConfigResult.model ?? 'gpt-4o',
|
|
287
|
+
apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
|
|
288
|
+
maxRetries: runtimeConfigResult.maxRetries,
|
|
289
|
+
maxTokens: runtimeConfigResult.maxTokens,
|
|
290
|
+
timeoutMs: runtimeConfigResult.timeoutMs,
|
|
291
|
+
baseUrl: runtimeConfigResult.baseUrl,
|
|
292
|
+
workspace: workspaceDir,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if (runtimeKind === 'openclaw-cli') {
|
|
297
|
+
adapter = new OpenClawCliRuntimeAdapter({
|
|
298
|
+
runtimeMode: runtimeConfigResult.openclawMode ?? 'default',
|
|
299
|
+
workspaceDir: workspaceDir,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
throw new Error(`Unsupported runtime kind resolved for auto-consumer: ${runtimeKind}`);
|
|
304
|
+
}
|
|
305
|
+
const { taskId } = wakeResult;
|
|
306
|
+
const { taskKind } = wakeResult;
|
|
307
|
+
// Issue 2: forward effectiveConfig so runners can resolve feature flags
|
|
308
|
+
// (e.g. `artificer_output_retry`) — mirrors the diagnostician wiring
|
|
309
|
+
// (ADR-0019). Flag-off / absent = legacy behavior.
|
|
310
|
+
const runnerOptions = { owner, runtimeKind, effectiveConfig: configResult.effective };
|
|
311
|
+
// PRI-634 A3: workspace-scoped telemetry sink for the evaluator runner.
|
|
312
|
+
// Constructed here (per-wake, workspaceDir in scope) so events from THIS
|
|
313
|
+
// workspace's runner are attributable to THIS workspace — a global
|
|
314
|
+
// subscriber on the storeEmitter singleton cannot (multi-workspace
|
|
315
|
+
// isolation, see workspace-telemetry-emitter.ts). Persist failures
|
|
316
|
+
// degrade through the host's structured event port, never break the runner.
|
|
317
|
+
const evaluatorEmitter = new WorkspaceTelemetryEmitter(storeEmitter, workspaceDir, (detail) => {
|
|
318
|
+
emitEvent('WORKSPACE_TELEMETRY_PERSIST_FAILED', detail);
|
|
319
|
+
});
|
|
320
|
+
// Dispatch by leased task kind. Only kinds listed in
|
|
321
|
+
// FULL_CHAIN_CONSUMER_RUNNER_KINDS can be leased here; anything else
|
|
322
|
+
// (e.g. diagnostician) hits the default branch — fail loud if it does (EP-03).
|
|
323
|
+
let runner;
|
|
324
|
+
switch (taskKind) {
|
|
325
|
+
case 'dreamer':
|
|
326
|
+
runner = new DreamerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDreamerValidator(), contentHashFn }, runnerOptions);
|
|
327
|
+
break;
|
|
328
|
+
case 'philosopher':
|
|
329
|
+
runner = new PhilosopherRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultPhilosopherValidator(), contentHashFn }, runnerOptions);
|
|
330
|
+
break;
|
|
331
|
+
case 'scribe':
|
|
332
|
+
runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator(), contentHashFn }, runnerOptions);
|
|
333
|
+
break;
|
|
334
|
+
case 'artificer':
|
|
335
|
+
runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator(), contentHashFn }, runnerOptions);
|
|
336
|
+
break;
|
|
337
|
+
case 'evaluator':
|
|
338
|
+
// P0-D 生产接线: PRI-509 repair loop 正式进入 consumer (bounded,
|
|
339
|
+
// flag evaluator_artificer_repair_loop 保留运行时关闭能力)。needs_revision
|
|
340
|
+
// → seed artificer repair; commit 门控保证不再并行 seed rollout_reviewer。
|
|
341
|
+
// PRI-630: 注入宿主声明的 runtime-authoritative 工具目录 — 工具名合法
|
|
342
|
+
// 性以目录为准,禁止 LLM 凭记忆判 "非标准工具名" (链 48371236 根因②)。
|
|
343
|
+
// PRI-634 A1/A3: (a) 注入 canonical production gateDeps — 此前缺省导致
|
|
344
|
+
// adversarial replay 结构性不可达 (链 48371236 根因 A1); (b) evaluator
|
|
345
|
+
// 的事件改走 workspace-scoped emitter, 只把 4 类 critical events 落盘到
|
|
346
|
+
// <workspaceDir>/.pd/telemetry/critical-events.jsonl, 其余照旧转发全局
|
|
347
|
+
// storeEmitter (multi-workspace 隔离, 见 workspace-telemetry-emitter.ts)。
|
|
348
|
+
// gateDeps 属于 evaluator production semantics, 不是宿主可选能力 —
|
|
349
|
+
// 无论 OpenClaw 还是 Codex worker 执行, deterministic gate wiring
|
|
350
|
+
// 必须存在 (第二个 options 参数, 绝不放第一个 deps 参数)。
|
|
351
|
+
runner = new EvaluatorRunner({
|
|
352
|
+
stateManager, runtimeAdapter: adapter, eventEmitter: evaluatorEmitter,
|
|
353
|
+
artifactStore: stateManager.piArtifactStore, validator: new DefaultEvaluatorValidator(),
|
|
354
|
+
...createEvaluatorRepairDeps(workspaceDir, stateManager, logger),
|
|
355
|
+
}, {
|
|
356
|
+
...runnerOptions,
|
|
357
|
+
gateDeps: createProductionGateDeps(),
|
|
358
|
+
...(hostToolCatalog
|
|
359
|
+
? { hostToolCatalog: { readOnlyTools: [...hostToolCatalog.readOnlyTools], writeTools: [...hostToolCatalog.writeTools] } }
|
|
360
|
+
: {}),
|
|
361
|
+
});
|
|
362
|
+
break;
|
|
363
|
+
case 'rollout_reviewer':
|
|
364
|
+
// Lineage echo reconciliation is built into the runner: LLM-truncated
|
|
365
|
+
// taskId / sourceEvaluatorArtifactId echoes are overwritten with the
|
|
366
|
+
// authoritative values before validation, so a bad echo no longer
|
|
367
|
+
// dead-ends the candidate before the approval queue.
|
|
368
|
+
//
|
|
369
|
+
// P0-E/F 治理接线: approve_rollout → 自动 ActivationDispatcher (低风险
|
|
370
|
+
// auto_activate / 高风险 approvals.pending); needs_revision → reopen
|
|
371
|
+
// scribe/artificer 修订 (绝不进入 approval 队列, INV-04)。
|
|
372
|
+
runner = new RolloutReviewerRunner({
|
|
373
|
+
stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter,
|
|
374
|
+
artifactStore: stateManager.piArtifactStore, validator: new DefaultRolloutReviewerValidator(),
|
|
375
|
+
...createRolloutGovernanceDeps(workspaceDir, orchestrator, logger),
|
|
376
|
+
}, runnerOptions);
|
|
377
|
+
break;
|
|
378
|
+
default:
|
|
379
|
+
emitEvent('INTERNALIZATION_CONSUMER_SKIP', JSON.stringify({ decision: 'no_runner_for_kind', taskKind }));
|
|
380
|
+
logger.warn(`[PD:${logLabel}] No consumer runner for task kind '${taskKind}'; skipping. Advance manually: pd runtime internalization run-once --runner ${taskKind}`);
|
|
381
|
+
return { ran: false, skipReason: 'no_runner_for_kind', taskKind };
|
|
382
|
+
}
|
|
383
|
+
logger.info(`[PD:${logLabel}] Running ${taskKind} task: ${taskId}`);
|
|
384
|
+
emitEvent('INTERNALIZATION_CONSUMER_RUN', JSON.stringify({
|
|
385
|
+
taskId,
|
|
386
|
+
taskKind,
|
|
387
|
+
}));
|
|
388
|
+
let runResult;
|
|
389
|
+
try {
|
|
390
|
+
runResult = await runner.run(taskId);
|
|
391
|
+
}
|
|
392
|
+
catch (runErr) {
|
|
393
|
+
logger.error(`[PD:${logLabel}] Runner crashed for task ${taskId}: ${String(runErr)}`);
|
|
394
|
+
try {
|
|
395
|
+
const task = await stateManager.getTask(taskId);
|
|
396
|
+
const failureReason = `Unhandled runner exception: ${runErr instanceof Error ? runErr.message : String(runErr)}`;
|
|
397
|
+
if (task && stateManager.getRetryPolicy().shouldRetry(task)) {
|
|
398
|
+
await stateManager.markTaskRetryWait(taskId, 'execution_failed', failureReason);
|
|
399
|
+
logger.info(`[PD:${logLabel}] Marked task ${taskId} as retry_wait.`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
await stateManager.markTaskFailed(taskId, 'execution_failed', failureReason);
|
|
403
|
+
logger.info(`[PD:${logLabel}] Marked task ${taskId} as failed.`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (dbErr) {
|
|
407
|
+
logger.error(`[PD:${logLabel}] Failed to update state for crashed task ${taskId}: ${String(dbErr)}`);
|
|
408
|
+
}
|
|
409
|
+
throw runErr;
|
|
410
|
+
}
|
|
411
|
+
if (runResult.status === 'succeeded') {
|
|
412
|
+
let commitResult = null;
|
|
413
|
+
try {
|
|
414
|
+
commitResult = await orchestrator.commitNextTaskProposal(taskId);
|
|
415
|
+
}
|
|
416
|
+
catch (commitErr) {
|
|
417
|
+
// The task itself succeeded — a successor-proposal failure must not
|
|
418
|
+
// misreport the run; surface it as its own structured event (rc-9).
|
|
419
|
+
emitEvent('INTERNALIZATION_CONSUMER_COMMIT_FAILED', JSON.stringify({ taskId, error: String(commitErr).slice(0, 300) }));
|
|
420
|
+
logger.warn(`[PD:${logLabel}] Task ${taskId} succeeded but successor proposal failed: ${String(commitErr).slice(0, 200)}`);
|
|
421
|
+
}
|
|
422
|
+
emitEvent('INTERNALIZATION_CONSUMER_SUCCESS', JSON.stringify({
|
|
423
|
+
taskId,
|
|
424
|
+
status: runResult.status,
|
|
425
|
+
...(commitResult ? { successorDecision: commitResult.decision } : { successorDecision: 'commit_failed' }),
|
|
426
|
+
}));
|
|
427
|
+
logger.info(`[PD:${logLabel}] Task ${taskId} succeeded. Successor: ${commitResult ? commitResult.decision : 'commit_failed'}`);
|
|
428
|
+
return { ran: true, taskId, taskKind, runStatus: 'succeeded' };
|
|
429
|
+
}
|
|
430
|
+
const { errorCategory } = runResult;
|
|
431
|
+
const { failureReason } = runResult;
|
|
432
|
+
const nextAction = getNextActionForError(errorCategory);
|
|
433
|
+
emitEvent('INTERNALIZATION_CONSUMER_TASK_FAILED', JSON.stringify({
|
|
434
|
+
taskId,
|
|
435
|
+
status: runResult.status,
|
|
436
|
+
errorCategory,
|
|
437
|
+
failureReason,
|
|
438
|
+
nextAction,
|
|
439
|
+
}));
|
|
440
|
+
logger.warn(`[PD:${logLabel}] Task ${taskId} status: ${runResult.status}. Category: ${errorCategory}. Reason: ${failureReason}. Next Action: ${nextAction}`);
|
|
441
|
+
return { ran: true, taskId, taskKind, runStatus: runResult.status === 'retried' ? 'retried' : 'failed' };
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
emitEvent('INTERNALIZATION_CONSUMER_ERROR', String(err));
|
|
445
|
+
logger.error(`[PD:${logLabel}] Cycle error: ${String(err)}`);
|
|
446
|
+
return { ran: false, skipReason: 'cycle_error' };
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
// PRI-554: per-cycle expired-lease recovery sweep. Worker process death
|
|
450
|
+
// mid-lease leaves tasks invisible to findCandidates (pending/retry_wait
|
|
451
|
+
// only); without this sweep they stay leased forever. Runs before
|
|
452
|
+
// reconciliation and is gated on `handle` (not `orchestrator`) so it still
|
|
453
|
+
// executes if the orchestrator constructor threw — enumerate every early
|
|
454
|
+
// return between resource-open and the finally steps (ERR-024).
|
|
455
|
+
if (handle) {
|
|
456
|
+
await safeRunRecoverySweep(handle.stateManager, ports);
|
|
457
|
+
}
|
|
458
|
+
// A (最终复核): 每周期固定小预算 — continuous backlog 下 reconciliation
|
|
459
|
+
// 不会被 ready 任务永久饿死 (公平性);游标 restart-durable。
|
|
460
|
+
// 必须在 handle.close() 之前执行 (orchestrator 共享该 stateManager)。
|
|
461
|
+
if (orchestrator) {
|
|
462
|
+
await runReconciliationBudget(workspaceDir, orchestrator, ports);
|
|
463
|
+
}
|
|
464
|
+
if (handle) {
|
|
465
|
+
await handle.close().catch(() => undefined);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|