@principles/host-runtime 0.7.10 → 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.7.10",
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.5",
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"