@principles/host-runtime 0.7.11 → 0.7.13

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/dist/index.d.ts CHANGED
@@ -66,6 +66,8 @@ export declare function createProductionHostRuntime(options?: Partial<Pick<HostR
66
66
  painDatabaseFactory?: PainDatabaseFactory;
67
67
  /** PRI-640: host attribution supplied by the constructing host adapter (OpenClaw / Codex). */
68
68
  hostKind?: GovernanceHostKind;
69
+ /** Host project cwd, distinct from the workspace owning shared PD state. */
70
+ projectDir?: string;
69
71
  /** PRI-634-F: host-declared tool semantics supplied by the constructing host adapter. */
70
72
  toolSemantics?: ToolSemanticRegistry;
71
73
  /**
package/dist/index.js CHANGED
@@ -163,6 +163,7 @@ export function createHostRuntime(options) {
163
163
  }
164
164
  export function createProductionHostRuntime(options = {}) {
165
165
  const productionGate = createProductionRuleHostGate({
166
+ ...(options.projectDir ? { projectDir: options.projectDir } : {}),
166
167
  ...(options.ruleContextProvider ? { ruleContextProvider: options.ruleContextProvider } : {}),
167
168
  ...(options.ruleInputEnrichmentProvider ? { ruleInputEnrichmentProvider: options.ruleInputEnrichmentProvider } : {}),
168
169
  ...(options.ruleImplementationRuntime ? { implementationRuntime: options.ruleImplementationRuntime } : {}),
@@ -16,6 +16,7 @@ export interface RuleInputEnrichment {
16
16
  }
17
17
  export type RuleInputEnrichmentProvider = (request: ProductionRuleContextRequest) => unknown | Promise<unknown>;
18
18
  export interface ProductionRuleHostGateOptions {
19
+ projectDir?: string;
19
20
  ruleContextProvider?: RuleContextProvider;
20
21
  ruleInputEnrichmentProvider?: RuleInputEnrichmentProvider;
21
22
  implementationRuntime?: RuleImplementationRuntime;
@@ -81,7 +81,7 @@ export function createProductionRuleHostGate(options = {}) {
81
81
  }
82
82
  const canonicalKind = toolSemantics.resolve(input.toolName);
83
83
  const { isBashTool: isBash, isWriteTool: isWrite } = deriveToolHintsFromCanonicalKind(canonicalKind);
84
- const action = buildRuleHostAction(input.toolName, input.params, event.context.workspaceDir, { isBashTool: isBash, isWriteTool: isWrite, canonicalKind });
84
+ const action = buildRuleHostAction(input.toolName, input.params, options.projectDir ?? event.context.workspaceDir, { isBashTool: isBash, isWriteTool: isWrite, canonicalKind });
85
85
  if (action.normalizedPath === null)
86
86
  return { decision: 'allow', source: event.source, metadata: { evaluatedLiveRules: 0 } };
87
87
  let context;
@@ -1,3 +1,4 @@
1
+ import { type PromptSelectionPolicy } from '@principles/core/runtime-v2';
1
2
  export interface PromptInjectionProjection {
2
3
  /** Which real injection route the workspace is on (abstraction_layer_v1 flag). */
3
4
  route: 'legacy_trim' | 'shared_render';
@@ -13,6 +14,38 @@ export interface PromptInjectionProjection {
13
14
  injectedPrincipleIds: string[];
14
15
  injectedActivationIds: string[];
15
16
  warnings: string[];
17
+ /**
18
+ * PR-1894: the budget-packing policy that produced THIS projection
19
+ * (`legacy_fifo_prefix_v1` | `fair_rotation_v1`). The console must not
20
+ * describe a fair-rotation workspace in FIFO terms, so the policy travels
21
+ * with the projection instead of being re-derived by each consumer.
22
+ */
23
+ selectionPolicy: PromptSelectionPolicy;
24
+ /**
25
+ * PR-1894: activations that the production route will inject under SOME
26
+ * round key, i.e. activations that are merely rotated out of the current
27
+ * window rather than structurally starved.
28
+ *
29
+ * Reachability is a property of the SELECTION POLICY, not of any single
30
+ * round: a fair-rotation workspace reaches every non-oversized eligible
31
+ * entry within N turns. So this is computed under the production policy
32
+ * (fair rotation whenever the route actually rotates) and is therefore
33
+ * non-empty even for a caller that supplied no round key — which is
34
+ * exactly the console's situation. Empty only when the production route
35
+ * genuinely does not rotate (shared route) or nothing is eligible.
36
+ */
37
+ eventuallyInjectedActivationIds: string[];
38
+ /** Number of eligible candidates that reached the selector. */
39
+ eligibleCount: number;
40
+ /**
41
+ * PR-1894: whether the PRODUCTION route for this workspace rotates.
42
+ * The forecast's own `selectionPolicy` answers "which policy did THIS
43
+ * projection run", which is legacy for a console that holds no session
44
+ * round key; this answers "will the agent's real injection rotate",
45
+ * which is what determines whether an out-of-window activation is queued
46
+ * or starved. They differ exactly in the case this whole fix is about.
47
+ */
48
+ productionRotates: boolean;
16
49
  }
17
50
  /**
18
51
  * PR #1844 follow-up: forecast the prompt-injection budget from the SAME
@@ -31,8 +64,25 @@ export interface PromptInjectionProjection {
31
64
  * candidates against the legacy evolution ledger before trimming; the
32
65
  * console cannot replay that reducer read-only, so this projection may
33
66
  * forecast slightly MORE consumption than the real injection (never less).
67
+ *
68
+ * PR-1894 — round-key alignment. The plugin derives its fair-rotation round
69
+ * key from the CURRENT session's turn ordinal
70
+ * (`nextSessionTurnOrdinal`), which the console has no access to. Passing no
71
+ * key therefore does NOT make the forecast "more conservative": it selects a
72
+ * DIFFERENT policy (`legacy_fifo_prefix_v1`) than the one production runs, and
73
+ * legacy FIFO structurally excludes the newest activation from every
74
+ * truncated selection. The console must either supply the round key or
75
+ * report the policy honestly, so this function takes the key as an explicit
76
+ * input and exposes `selectionPolicy` on the result.
34
77
  */
35
78
  export declare function buildLivePromptInjectionProjection(input: {
36
79
  workspaceDir: string;
37
80
  excludePrincipleIds?: ReadonlySet<string>;
81
+ /**
82
+ * PR-1894: the fair-rotation round key the production route is using this
83
+ * turn. Callers that hold one (the plugin) pass it; callers that do not
84
+ * (the console) omit it and MUST read `selectionPolicy` off the result
85
+ * rather than assuming FIFO.
86
+ */
87
+ roundKey?: number;
38
88
  }): Promise<PromptInjectionProjection>;
@@ -2,6 +2,28 @@ import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, trimToBudge
2
2
  import { escapeXml } from '@principles/core/prompt-builder';
3
3
  import { loadPdConfigForPlugin } from './pd-config.js';
4
4
  import { buildActivePrinciplePromptContext, readPromptActivationCandidates } from './active-principle-prompt.js';
5
+ /**
6
+ * PR-1894: which activations a fair-rotation selector injects across the
7
+ * whole ring. The plugin's round key advances by one per recorded user turn
8
+ * within a continuously advancing session, so N consecutive turns cover all
9
+ * N ring positions: an activation absent from the current window but present
10
+ * in ANY round is reachable, not starved.
11
+ *
12
+ * Kept here (not in the console) so the production selector's own arithmetic
13
+ * stays the single authority for what "reachable" means.
14
+ */
15
+ function collectReachableActivationIds(principles, budget) {
16
+ const reachable = new Set();
17
+ for (let roundKey = 0; roundKey < principles.length; roundKey += 1) {
18
+ const result = trimToBudget(principles, budget, escapeXml, roundKey);
19
+ for (const principleId of result.injectedIds) {
20
+ const match = principles.find((p) => p.principleId === principleId);
21
+ if (match)
22
+ reachable.add(match.activationId);
23
+ }
24
+ }
25
+ return [...reachable];
26
+ }
5
27
  /**
6
28
  * PR #1844 follow-up: forecast the prompt-injection budget from the SAME
7
29
  * projection the agent injection itself uses, routed by the same
@@ -19,6 +41,16 @@ import { buildActivePrinciplePromptContext, readPromptActivationCandidates } fro
19
41
  * candidates against the legacy evolution ledger before trimming; the
20
42
  * console cannot replay that reducer read-only, so this projection may
21
43
  * forecast slightly MORE consumption than the real injection (never less).
44
+ *
45
+ * PR-1894 — round-key alignment. The plugin derives its fair-rotation round
46
+ * key from the CURRENT session's turn ordinal
47
+ * (`nextSessionTurnOrdinal`), which the console has no access to. Passing no
48
+ * key therefore does NOT make the forecast "more conservative": it selects a
49
+ * DIFFERENT policy (`legacy_fifo_prefix_v1`) than the one production runs, and
50
+ * legacy FIFO structurally excludes the newest activation from every
51
+ * truncated selection. The console must either supply the round key or
52
+ * report the policy honestly, so this function takes the key as an explicit
53
+ * input and exposes `selectionPolicy` on the result.
22
54
  */
23
55
  export async function buildLivePromptInjectionProjection(input) {
24
56
  const sharedRoute = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
@@ -32,6 +64,13 @@ export async function buildLivePromptInjectionProjection(input) {
32
64
  injectedPrincipleIds: context.principleIds,
33
65
  injectedActivationIds: context.activationIds,
34
66
  warnings: context.warnings,
67
+ // The shared production path deliberately passes no round key
68
+ // (host-runtime/src/index.ts), so this route genuinely does not rotate
69
+ // and claims nothing about eventual reachability.
70
+ selectionPolicy: context.selectionPolicy ?? 'legacy_fifo_prefix_v1',
71
+ eventuallyInjectedActivationIds: [],
72
+ eligibleCount: context.eligibleCount ?? 0,
73
+ productionRotates: false,
35
74
  };
36
75
  }
37
76
  const candidates = await readPromptActivationCandidates(input);
@@ -45,10 +84,23 @@ export async function buildLivePromptInjectionProjection(input) {
45
84
  injectedPrincipleIds: [],
46
85
  injectedActivationIds: [],
47
86
  warnings,
87
+ selectionPolicy: 'legacy_fifo_prefix_v1',
88
+ eventuallyInjectedActivationIds: [],
89
+ eligibleCount: 0,
90
+ productionRotates: false,
48
91
  };
49
92
  }
50
- const trimmed = trimToBudget(principles, RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml);
93
+ // PR-1894: pass the caller's round key so the forecast runs the SAME
94
+ // packing policy production runs this turn.
95
+ const trimmed = trimToBudget(principles, RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml, input.roundKey);
51
96
  const injected = principles.filter((p) => trimmed.injectedIds.has(p.principleId));
97
+ // PR-1894: the legacy_trim route IS the OpenClaw plugin-local route, which
98
+ // rotates on every recorded user turn (PRI-904). Reachability is therefore
99
+ // computed under fair rotation regardless of whether THIS forecast holds a
100
+ // round key — a console without a session key must still be able to tell
101
+ // "queued behind rotation" apart from "structurally starved", otherwise it
102
+ // keeps telling the Owner to deactivate healthy principles.
103
+ const eventuallyInjectedActivationIds = collectReachableActivationIds(principles, RUNTIME_V2_PRINCIPLE_BUDGET);
52
104
  return {
53
105
  route: 'legacy_trim',
54
106
  budget: RUNTIME_V2_PRINCIPLE_BUDGET,
@@ -57,5 +109,9 @@ export async function buildLivePromptInjectionProjection(input) {
57
109
  injectedPrincipleIds: [...trimmed.injectedIds],
58
110
  injectedActivationIds: injected.map((p) => p.activationId),
59
111
  warnings,
112
+ selectionPolicy: trimmed.selectionPolicy,
113
+ eventuallyInjectedActivationIds,
114
+ eligibleCount: trimmed.eligibleCount,
115
+ productionRotates: true,
60
116
  };
61
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
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.6",
24
+ "@principles/core": "^1.289.0",
25
25
  "@principles/install-layout": "^0.2.7",
26
26
  "better-sqlite3": "^13.0.3",
27
27
  "js-yaml": "^5.4.1"