@principles/host-runtime 0.7.8 → 0.7.11

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.
@@ -1,4 +1,4 @@
1
- import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
1
+ import { type ActivatedPrinciple, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
2
2
  export interface ActivePrinciplePromptResult {
3
3
  additionalContext: string;
4
4
  principleIds: string[];
@@ -11,6 +11,16 @@ export interface ActivePrinciplePromptResult {
11
11
  excludedCount: number;
12
12
  exclusionReason?: 'host_principle_overlap';
13
13
  allValidatedPrinciplesExcluded: boolean;
14
+ /** PRI-904: budget-packing policy that produced this selection. */
15
+ selectionPolicy?: PromptSelectionPolicy;
16
+ /** PRI-904: candidates that reached the budget selector. */
17
+ eligibleCount?: number;
18
+ /** PRI-904: circular scan start; present only under fair_rotation_v1. */
19
+ rotationStartIndex?: number;
20
+ /** PRI-904 (bounded, max 16): activation ids dropped for insufficient remaining budget. */
21
+ droppedActivationIds?: string[];
22
+ /** PRI-904 (bounded, max 16): activation ids that cannot fit even in an empty payload. */
23
+ oversizedActivationIds?: string[];
14
24
  }
15
25
  export interface PromptActivationCandidates {
16
26
  principles: ActivatedPrinciple[];
@@ -34,4 +44,10 @@ export declare function readPromptActivationCandidates(input: {
34
44
  export declare function buildActivePrinciplePromptContext(input: {
35
45
  workspaceDir: string;
36
46
  excludePrincipleIds?: ReadonlySet<string>;
47
+ /**
48
+ * PRI-904 fair-rotation round key (derive via roundKeyFromRunIdentity from
49
+ * the host run/turn id). Absent → legacy FIFO prefix policy (rollback /
50
+ * characterization baseline).
51
+ */
52
+ roundKey?: number;
37
53
  }): Promise<ActivePrinciplePromptResult>;
@@ -3,6 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { escapeXml } from '@principles/core/prompt-builder';
5
5
  import { loadPdConfigForPlugin } from './pd-config.js';
6
+ /** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
7
+ const MAX_SHARED_DIAGNOSTIC_IDS = 16;
6
8
  /**
7
9
  * PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
8
10
  * exported so the console budget projection can share the exact same input
@@ -85,17 +87,49 @@ export async function buildActivePrinciplePromptContext(input) {
85
87
  additionalContext: '', principleIds: [], activationIds: [], artifactIds: [], warnings,
86
88
  budget: RUNTIME_V2_PRINCIPLE_BUDGET, truncated: false, excludedPrincipleIds,
87
89
  excludedCount: excludedPrincipleIds.length, allValidatedPrinciplesExcluded: false,
90
+ selectionPolicy: 'legacy_fifo_prefix_v1', eligibleCount: 0,
88
91
  };
89
92
  }
90
93
  const included = [];
91
94
  let additionalContext = '';
92
95
  let truncated = false;
93
- for (const principle of principles) {
96
+ // PRI-904: same fair-rotation semantics as the plugin route's trimToBudget —
97
+ // deterministic rotating start over the base (ASC) candidate order, circular
98
+ // scan, continue-on-non-fit. Costs are measured with the REAL serializer
99
+ // (renderPrinciplesToDirectives) so budget accounting stays exact here too.
100
+ const { roundKey } = input;
101
+ const selectionPolicy = roundKey === undefined || principles.length === 0 ? 'legacy_fifo_prefix_v1' : 'fair_rotation_v1';
102
+ const n = principles.length;
103
+ // Guard on the policy, not just the key: an all-excluded candidate list has
104
+ // n === 0 and must not emit a NaN start (core trimToBudget omits the field
105
+ // for empty input — parity requires the same here).
106
+ const rotationStartIndex = selectionPolicy === 'fair_rotation_v1' && roundKey !== undefined
107
+ ? ((roundKey % n) + n) % n
108
+ : undefined;
109
+ const droppedActivationIds = [];
110
+ const oversizedActivationIds = [];
111
+ const standaloneFits = (principle) => renderPrinciplesToDirectives([principle], new Set([principle.principleId]), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled }).length
112
+ <= RUNTIME_V2_PRINCIPLE_BUDGET;
113
+ const scanOrder = rotationStartIndex === undefined
114
+ ? principles
115
+ : principles.slice(rotationStartIndex).concat(principles.slice(0, rotationStartIndex));
116
+ for (const principle of scanOrder) {
94
117
  const candidate = [...included, principle];
95
118
  const candidateContext = renderPrinciplesToDirectives(candidate, new Set(candidate.map((entry) => entry.principleId)), { escapeFn: escapeXml, selfReportInstruction: selfReportEnabled });
96
119
  if (candidateContext.length > RUNTIME_V2_PRINCIPLE_BUDGET) {
97
- truncated = true;
98
- break;
120
+ if (standaloneFits(principle)) {
121
+ // Fits alone but not now — normal budget truncation.
122
+ truncated = true;
123
+ if (droppedActivationIds.length < MAX_SHARED_DIAGNOSTIC_IDS)
124
+ droppedActivationIds.push(principle.activationId);
125
+ }
126
+ else if (oversizedActivationIds.length < MAX_SHARED_DIAGNOSTIC_IDS) {
127
+ // Can never fit alone — oversize, not starvation (PRI-904 SPEC §6.6).
128
+ oversizedActivationIds.push(principle.activationId);
129
+ }
130
+ if (selectionPolicy === 'legacy_fifo_prefix_v1')
131
+ break;
132
+ continue;
99
133
  }
100
134
  included.push(principle);
101
135
  additionalContext = candidateContext;
@@ -116,5 +150,10 @@ export async function buildActivePrinciplePromptContext(input) {
116
150
  excludedCount: excludedPrincipleIds.length,
117
151
  allValidatedPrinciplesExcluded: excludedPrincipleIds.length > 0 && principles.length === 0,
118
152
  ...(excludedPrincipleIds.length > 0 ? { exclusionReason: 'host_principle_overlap' } : {}),
153
+ selectionPolicy,
154
+ eligibleCount: principles.length,
155
+ ...(rotationStartIndex !== undefined ? { rotationStartIndex } : {}),
156
+ ...(droppedActivationIds.length > 0 ? { droppedActivationIds } : {}),
157
+ ...(oversizedActivationIds.length > 0 ? { oversizedActivationIds } : {}),
119
158
  };
120
159
  }
package/dist/index.js CHANGED
@@ -177,6 +177,16 @@ export function createProductionHostRuntime(options = {}) {
177
177
  }),
178
178
  beforeToolCall: options.beforeToolCall ?? productionGate,
179
179
  async beforePromptBuild(event) {
180
+ // PRI-904 Phase-1 (Option 3): this production path is ALSO the Codex
181
+ // route, and Codex never writes trajectory.db::user_turns — its
182
+ // ingestion writes governance_* rows instead. Reading a session turn
183
+ // ordinal here would therefore be a false round authority: on Codex it
184
+ // either throws (→ legacy) or returns a constant 1 (→ fixed start, a
185
+ // different positional starvation while the event still claimed a fair
186
+ // rotation). So the shared production path passes NO round key and
187
+ // reports `legacy_fifo_prefix_v1` honestly. The selector's fair-rotation
188
+ // capability is retained and stays reachable for callers that DO hold a
189
+ // legitimate advancing round authority.
180
190
  const prompt = await buildActivePrinciplePromptContext({
181
191
  workspaceDir: event.context.workspaceDir,
182
192
  excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
@@ -203,6 +213,14 @@ export function createProductionHostRuntime(options = {}) {
203
213
  budget: RUNTIME_V2_PRINCIPLE_BUDGET,
204
214
  ...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
205
215
  ...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
216
+ // PRI-904: selection diagnostics (bounded, optional). On this route
217
+ // the policy is legacy_fifo_prefix_v1 and NO rotation provenance is
218
+ // emitted — a fair claim would be untrue here (see above).
219
+ ...(prompt.selectionPolicy !== undefined ? { selectionPolicy: prompt.selectionPolicy } : {}),
220
+ ...(prompt.eligibleCount !== undefined ? { eligibleCount: prompt.eligibleCount } : {}),
221
+ ...(prompt.rotationStartIndex !== undefined ? { rotationStartIndex: prompt.rotationStartIndex } : {}),
222
+ ...(prompt.droppedActivationIds !== undefined && prompt.droppedActivationIds.length > 0 ? { droppedActivationIds: prompt.droppedActivationIds } : {}),
223
+ ...(prompt.oversizedActivationIds !== undefined && prompt.oversizedActivationIds.length > 0 ? { oversizedActivationIds: prompt.oversizedActivationIds } : {}),
206
224
  });
207
225
  }
208
226
  catch (err) {
@@ -20,7 +20,7 @@
20
20
  * Hosts inject: a logger, a structured-event sink, their tool catalog, and an
21
21
  * env getter. Everything else is the existing production path.
22
22
  */
23
- import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE,
23
+ import { createRuntimeStateHandle, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, buildArtificerHostSemanticContext, EvaluatorRunner, RolloutReviewerRunner, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, PiAiRuntimeAdapter, L2AgentLoopAdapter, ArtificerL2Adapter, buildL2PrincipleReaderFromLedger, OpenClawCliRuntimeAdapter, storeEmitter, createProductionGateDeps, resolveRuntimeConfigFromPdConfig, resolveRuntimeConfigForAgent, AGENT_NAME_FOR_TASK_KIND, isRuntimeConfigError, computeConsumerDecision, FULL_CHAIN_CONSUMER_RUNNER_KINDS, DEFAULT_CONSUMER_RUNNER_KINDS, InternalizationQueueReadModel, MVP_CORE_TASK_KINDS, SqliteConnection, SqliteReconciliationCursorStore, SUCCEEDED_TRANSITIONS_SCOPE, PrincipleTreeLedgerAdapter,
24
24
  // PRI-714: resolve outputLanguage from the same effective config the
25
25
  // runners already receive (EP-07: canonical resolved value, not raw input).
26
26
  resolveOutputLanguage, } from '@principles/core/runtime-v2';
@@ -459,7 +459,18 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
459
459
  runner = new PhilosopherRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultPhilosopherValidator() }, runnerOptions);
460
460
  break;
461
461
  case 'scribe':
462
- runner = new ScribeRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator() }, runnerOptions);
462
+ runner = new ScribeRunner({
463
+ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultScribeValidator(),
464
+ // I2 — CHAIN STAMPING (Owner review of PR #1856, P1): the scribe
465
+ // stamps the ledger principle UUID (minted at intake, keyed by the
466
+ // dreamer task seed's candidateId) onto the artifact it writes, so
467
+ // the dispatcher's ledger identity gate sees a resolvable
468
+ // identity on the live production chain. listForCandidate reads
469
+ // the ledger from disk per call — no cross-cycle staleness.
470
+ ledgerIdentity: {
471
+ listForCandidate: (candidateId) => new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }).listForCandidate(candidateId),
472
+ },
473
+ }, runnerOptions);
463
474
  break;
464
475
  case 'artificer':
465
476
  runner = new ArtificerRunner({ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultArtificerValidator() },
@@ -483,6 +494,14 @@ export async function runInternalizationConsumerCycle(workspaceDir, ports) {
483
494
  runner = new EvaluatorRunner({
484
495
  stateManager, runtimeAdapter: adapter, eventEmitter: workspaceEmitter,
485
496
  artifactStore: stateManager.piArtifactStore, validator: new DefaultEvaluatorValidator(),
497
+ // PRI-911A (I1 writer boundary): the rule artifact's identity is a
498
+ // ledger UUID or nothing. hasPrinciple turns "UUID-shaped" into
499
+ // "UUID the ledger actually mints", so a shaped-but-unknown id is
500
+ // refused as invalid_identity instead of being carried forward.
501
+ // Same per-call disk read as the scribe wiring above.
502
+ ledgerIdentity: {
503
+ hasPrinciple: (principleId) => new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }).hasPrinciple(principleId),
504
+ },
486
505
  ...createEvaluatorRepairDeps(workspaceDir, stateManager, logger),
487
506
  }, {
488
507
  ...runnerOptions,
@@ -14,7 +14,7 @@
14
14
  * dedupes by `${artifactId}::${channel}`; reopen idempotency is owned by
15
15
  * orchestrator.reopenTaskForRevision.
16
16
  */
17
- import { ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, SqliteConnection, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, createProductionGateDeps, createPITaskDiagnosticJson, artificerRepairTaskId, computeFeatureFlagsFromConfig, isFeatureEnabled, } from '@principles/core/runtime-v2';
17
+ import { ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, SqliteConnection, SqliteActivationStateStore, SqliteApprovalQueueStore, SqlitePIArtifactStore, PrincipleTreeLedgerAdapter, createProductionGateDeps, createPITaskDiagnosticJson, artificerRepairTaskId, computeFeatureFlagsFromConfig, isFeatureEnabled, } from '@principles/core/runtime-v2';
18
18
  import { loadPdConfigForPlugin } from './pd-config.js';
19
19
  function normalizeDecision(decision) {
20
20
  if (decision.decision === 'activated') {
@@ -64,6 +64,27 @@ export async function dispatchRolloutActivation(workspaceDir, input, options = {
64
64
  const activationStateStore = new SqliteActivationStateStore(connection);
65
65
  const approvalQueueStore = new SqliteApprovalQueueStore(connection);
66
66
  const flagProbe = makeFlagProbe(workspaceDir);
67
+ // I3 upgrade (Owner review of PR #1856, P1): the production dispatch path
68
+ // verifies ledger membership BEFORE the approval record and the activation
69
+ // commit. The ledger SSOT lives in <workspace>/.state (same dir the intake
70
+ // used to mint the principle UUID); candidate lineage reuses the artifact
71
+ // read model plus raw diagnostic_json reads.
72
+ const ledgerIdentity = {
73
+ ledger: new PrincipleTreeLedgerAdapter({ stateDir: `${workspaceDir}/.state` }),
74
+ getArtifactById: artifactReadModel.getArtifactById,
75
+ getTaskDiagnosticJson: (taskId) => {
76
+ try {
77
+ const row = connection
78
+ .getDb()
79
+ .prepare('SELECT diagnostic_json FROM tasks WHERE task_id = ?')
80
+ .get(taskId);
81
+ return row?.diagnostic_json ?? null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ },
87
+ };
67
88
  const dispatcher = new ActivationDispatcher(artifactReadModel, activationStateStore, {
68
89
  writers: [
69
90
  new PromptWriter(),
@@ -79,6 +100,7 @@ export async function dispatchRolloutActivation(workspaceDir, input, options = {
79
100
  new DeferArchiveWriter(),
80
101
  ],
81
102
  approvalQueueStore,
103
+ ledgerIdentity,
82
104
  });
83
105
  const decision = await dispatcher.dispatch({
84
106
  artifactId: input.artifactId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.7.8",
3
+ "version": "0.7.11",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "lint": "eslint \"src/**/*.ts\""
22
22
  },
23
23
  "dependencies": {
24
- "@principles/core": "^1.287.2",
24
+ "@principles/core": "^1.287.6",
25
25
  "@principles/install-layout": "^0.2.7",
26
26
  "better-sqlite3": "^13.0.3",
27
27
  "js-yaml": "^5.4.1"