@principles/host-runtime 0.11.0 → 0.12.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/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/prompt-injection-budget-observation.d.ts +176 -0
- package/dist/prompt-injection-budget-observation.js +449 -0
- package/dist/prompt-injection-projection.d.ts +16 -0
- package/dist/prompt-injection-projection.js +18 -3
- package/dist/prompt-injection-status.d.ts +28 -1
- package/dist/prompt-injection-status.js +21 -7
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './prompt-injection-projection.js';
|
|
|
9
9
|
export * from './prompt-activation-deliverability.js';
|
|
10
10
|
export * from './prompt-injection-status.js';
|
|
11
11
|
export * from './prompt-injection-event-evidence.js';
|
|
12
|
+
export * from './prompt-injection-budget-observation.js';
|
|
12
13
|
export * from './pd-config.js';
|
|
13
14
|
export * from './production-rulehost-gate.js';
|
|
14
15
|
export * from './rule-implementation-runtime.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,10 @@ export * from './prompt-injection-status.js';
|
|
|
15
15
|
// SPEC PD_PROMPT_CAPACITY_V1 Phase C (AC-12): honest read-model over the
|
|
16
16
|
// existing injection-event JSONL — unknown vs proven-zero never conflated.
|
|
17
17
|
export * from './prompt-injection-event-evidence.js';
|
|
18
|
+
// PRI-1017: runtime budget OBSERVATION read model over the SAME injection
|
|
19
|
+
// events — console surfaces forecast under the budget the last real
|
|
20
|
+
// injection used (SPEC §6.1), labelled with source + observation time.
|
|
21
|
+
export * from './prompt-injection-budget-observation.js';
|
|
18
22
|
export * from './pd-config.js';
|
|
19
23
|
export * from './production-rulehost-gate.js';
|
|
20
24
|
export * from './rule-implementation-runtime.js';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { PROMPT_BUDGET_POLICY_VERSION, type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution } from '@principles/core/runtime-v2';
|
|
2
|
+
import { type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
|
+
/**
|
|
4
|
+
* PRI-1017: runtime budget OBSERVATION read model over the EXISTING
|
|
5
|
+
* injection-event JSONL (`<workspaceDir>/{.pd,.state}/logs/events_<date>.jsonl`,
|
|
6
|
+
* written by the OpenClaw EventLog and the Codex/WorkBuddy shared path via
|
|
7
|
+
* appendEventLogLine).
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 forbids the
|
|
10
|
+
* actual injection from running a dynamic budget while the Console forecasts
|
|
11
|
+
* under a different fixed 2000-char budget. The console holds no live host
|
|
12
|
+
* facts, but the RUNTIME persists the resolution it actually used —
|
|
13
|
+
* `runtime_v2_prompt_activations_injected` carries budgetChars, capacity
|
|
14
|
+
* provenance and hostKind. This module reads those facts back so console
|
|
15
|
+
* surfaces forecast under the same budget the last real injection used.
|
|
16
|
+
*
|
|
17
|
+
* Honesty rules this model enforces (SPEC §6.1/§7):
|
|
18
|
+
* - An observation is a FACT ABOUT THE PAST, never a live claim: callers must
|
|
19
|
+
* surface `observedAt`; the console labels it "most recent runtime
|
|
20
|
+
* observation", not a current confirmation.
|
|
21
|
+
* - No logs / no budget facts ⇒ absent, never a fabricated window; the caller
|
|
22
|
+
* keeps the honest fixed-fallback policy.
|
|
23
|
+
* - A latest observation older than RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS is
|
|
24
|
+
* `stale` — the caller must NOT present it as the current budget.
|
|
25
|
+
* - Observations bind to ONE host: OpenClaw's window is never used for a
|
|
26
|
+
* Codex or WorkBuddy forecast.
|
|
27
|
+
* - Untrusted input discipline: every line is parsed as `unknown` and
|
|
28
|
+
* narrowed with typeof guards + closed-set checks; structurally invalid
|
|
29
|
+
* lines are counted in `parseFailures` and skipped loudly, never guessed.
|
|
30
|
+
* - Events predating the model-aware policy carry no provenance fields; they
|
|
31
|
+
* are counted as `legacyEventsSkipped` and produce NO observation — a
|
|
32
|
+
* missing provenance field is "policy unknown", never a capacity claim.
|
|
33
|
+
* - Bounded I/O: only the newest bytes of the most recent daily files are read
|
|
34
|
+
* (tail window), the file cap applying PER candidate log directory, so a
|
|
35
|
+
* runaway or crafted log cannot make a Console request read an unbounded
|
|
36
|
+
* amount of data. Only bounded budget metadata is retained and returned;
|
|
37
|
+
* other event payloads are not cached.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Candidate directories for the runtime-v2 event log, in priority order.
|
|
41
|
+
* Shared with the RuleCode telemetry collector (ERR-031: both readers derive
|
|
42
|
+
* from one list — the v1 EventLog writer emits `.state/logs`, no production
|
|
43
|
+
* code ever created `.pd/logs`).
|
|
44
|
+
*/
|
|
45
|
+
export declare const RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Bounded I/O: only the most recent daily files are scanned — PER candidate
|
|
48
|
+
* log directory (`RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`), not globally. The
|
|
49
|
+
* cap is deliberately per directory: both directories describe the same
|
|
50
|
+
* workspace's log convention and either can hold the newest event for a given
|
|
51
|
+
* host (the reader takes latest-wins ACROSS them), so a global cap would let
|
|
52
|
+
* one directory's files starve the other and drop a live host observation.
|
|
53
|
+
* Worst case per read is therefore `RUNTIME_BUDGET_OBSERVATION_MAX_FILES ×
|
|
54
|
+
* RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS.length` files (14 today) and the same
|
|
55
|
+
* count × `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` bytes (56 MiB today).
|
|
56
|
+
*/
|
|
57
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILES = 7;
|
|
58
|
+
/**
|
|
59
|
+
* Bounded I/O: per file, only the newest bytes are read. The writer appends,
|
|
60
|
+
* so the latest observation per host lives at the TAIL; a file larger than
|
|
61
|
+
* this window is tail-read and its older prefix skipped (counted in
|
|
62
|
+
* `truncatedFiles`) instead of materialising an unbounded log in the console
|
|
63
|
+
* process. 4 MiB is ~3.4x the largest real day observed on 2026-10-21
|
|
64
|
+
* (WorkBuddy 1.17 MiB, Codex 0.87 MiB, OpenClaw 0.98 MiB across 7 files).
|
|
65
|
+
*/
|
|
66
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum age of an observation that may be presented as a CURRENT runtime
|
|
69
|
+
* fact. The runtime refreshes the observation on every prompt build, so an
|
|
70
|
+
* active workspace holds minute-old facts; beyond a day the model may have
|
|
71
|
+
* been switched and the window is no longer evidence about the present.
|
|
72
|
+
*/
|
|
73
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS: number;
|
|
74
|
+
/**
|
|
75
|
+
* Clock-skew allowance for event timestamps. A writer clock ahead of ours is
|
|
76
|
+
* normal; an observation further in the future is not a fact about the
|
|
77
|
+
* present and must never be selected as the current budget.
|
|
78
|
+
*/
|
|
79
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS: number;
|
|
80
|
+
/** Console-side bounded reason: a runtime observation exists but is too old. */
|
|
81
|
+
export declare const RUNTIME_BUDGET_OBSERVATION_STALE_REASON: 'runtime_budget_observation_stale';
|
|
82
|
+
export interface RuntimePromptBudgetObservation {
|
|
83
|
+
/** The workspace the observed event was bound to (event `data.workspaceDir`). */
|
|
84
|
+
workspaceDir: string;
|
|
85
|
+
/** Host that produced the observation; observations never cross hosts. */
|
|
86
|
+
hostKind: PromptInjectionTargetHost;
|
|
87
|
+
/** Budget the real injection ran under (UTF-16 code units). */
|
|
88
|
+
budgetChars: number;
|
|
89
|
+
capacitySource: PromptBudgetCapacitySource;
|
|
90
|
+
capacityStatus: PromptBudgetCapacityStatus;
|
|
91
|
+
/** The context window the budget derived from; null when unknown. */
|
|
92
|
+
contextWindowTokens: number | null;
|
|
93
|
+
/** Bounded fallback reason carried by the event; null when confirmed. */
|
|
94
|
+
fallbackReason: PromptBudgetFallbackReason | null;
|
|
95
|
+
/** Policy version the observed budget was computed under (closed literal). */
|
|
96
|
+
policyVersion: typeof PROMPT_BUDGET_POLICY_VERSION | null;
|
|
97
|
+
/** ISO timestamp of the observed event (`ts` envelope field). */
|
|
98
|
+
observedAt: string;
|
|
99
|
+
}
|
|
100
|
+
export interface RuntimeBudgetObservationReport {
|
|
101
|
+
/** Latest observation per host (most recent by event `ts`). */
|
|
102
|
+
observations: RuntimePromptBudgetObservation[];
|
|
103
|
+
filesRead: string[];
|
|
104
|
+
lines: number;
|
|
105
|
+
parseFailures: number;
|
|
106
|
+
/** Injection events without model-aware budget provenance (pre-policy events). */
|
|
107
|
+
legacyEventsSkipped: number;
|
|
108
|
+
/** Injection events whose budget policy version differs from the current one. */
|
|
109
|
+
foreignPolicyEventsSkipped: number;
|
|
110
|
+
/**
|
|
111
|
+
* Files larger than the bounded read window whose older prefix was skipped.
|
|
112
|
+
* Non-zero means `lines`/skip counters describe the read window only.
|
|
113
|
+
*/
|
|
114
|
+
truncatedFiles: number;
|
|
115
|
+
note: string;
|
|
116
|
+
}
|
|
117
|
+
export type RuntimeBudgetObservationSelection = {
|
|
118
|
+
kind: 'usable';
|
|
119
|
+
observation: RuntimePromptBudgetObservation;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'stale';
|
|
122
|
+
observation: RuntimePromptBudgetObservation;
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'absent';
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Read the most recent budget observation per host from the workspace's
|
|
128
|
+
* runtime-v2 event logs. Never throws: an unreadable log directory degrades
|
|
129
|
+
* to an empty report with an explicit note (absence is UNKNOWN, not zero).
|
|
130
|
+
*/
|
|
131
|
+
export declare function readRuntimePromptBudgetObservations(input: {
|
|
132
|
+
workspaceDir: string;
|
|
133
|
+
/** Reference clock for the future-timestamp guard (tests pass a fixed one). */
|
|
134
|
+
now?: Date;
|
|
135
|
+
}): RuntimeBudgetObservationReport;
|
|
136
|
+
/**
|
|
137
|
+
* Select the observation that may back a console forecast for `targetHost`.
|
|
138
|
+
* `absent` when the host has no observation at all; `stale` when the latest
|
|
139
|
+
* one is older than the freshness bound (the caller must fall back honestly).
|
|
140
|
+
*/
|
|
141
|
+
export declare function selectRuntimeBudgetObservation(input: {
|
|
142
|
+
observations: readonly RuntimePromptBudgetObservation[];
|
|
143
|
+
targetHost?: PromptInjectionTargetHost;
|
|
144
|
+
now?: Date;
|
|
145
|
+
}): RuntimeBudgetObservationSelection;
|
|
146
|
+
/**
|
|
147
|
+
* Console-facing selection: one call gives the confirmed-host budget basis,
|
|
148
|
+
* the honest stale marker, and the per-host bases for unconfirmed routes.
|
|
149
|
+
* Reads the workspace's event logs; never throws.
|
|
150
|
+
*/
|
|
151
|
+
export interface ConsoleRuntimeBudgetFacts {
|
|
152
|
+
/** Usable observation for the requested host (only when targetHost is given). */
|
|
153
|
+
selected?: RuntimePromptBudgetObservation;
|
|
154
|
+
/** Latest observation for the requested host that is too old to be current. */
|
|
155
|
+
stale?: RuntimePromptBudgetObservation;
|
|
156
|
+
/**
|
|
157
|
+
* Usable observations per host — used for unconfirmed per-host forecasts
|
|
158
|
+
* AND for a confirmed route whose host was resolved from workspace facts
|
|
159
|
+
* (no request-level targetHost). Each host only ever gets its own fact.
|
|
160
|
+
*/
|
|
161
|
+
perHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
|
|
162
|
+
/** Latest per-host observation that is too old to be presented as current. */
|
|
163
|
+
stalePerHost: Map<PromptInjectionTargetHost, RuntimePromptBudgetObservation>;
|
|
164
|
+
}
|
|
165
|
+
export declare function selectConsoleRuntimeBudgetFacts(input: {
|
|
166
|
+
workspaceDir: string;
|
|
167
|
+
targetHost?: PromptInjectionTargetHost;
|
|
168
|
+
now?: Date;
|
|
169
|
+
}): ConsoleRuntimeBudgetFacts;
|
|
170
|
+
/**
|
|
171
|
+
* Rebuild the budget resolution an observation stands for, so console
|
|
172
|
+
* projections run under the SAME resolution shape the real injection used.
|
|
173
|
+
* `fallbackDetail` is deliberately dropped: it is log-only context and never
|
|
174
|
+
* rides events or console payloads.
|
|
175
|
+
*/
|
|
176
|
+
export declare function runtimeBudgetObservationToResolution(observation: RuntimePromptBudgetObservation): PromptBudgetResolution;
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PROMPT_BUDGET_POLICY_VERSION, RUNTIME_V2_PRINCIPLE_BUDGET, isValidContextWindowTokens, promptBudgetCharsForContextWindow, } from '@principles/core/runtime-v2';
|
|
4
|
+
import { isPromptInjectionTargetHost, } from './prompt-injection-projection.js';
|
|
5
|
+
/**
|
|
6
|
+
* PRI-1017: runtime budget OBSERVATION read model over the EXISTING
|
|
7
|
+
* injection-event JSONL (`<workspaceDir>/{.pd,.state}/logs/events_<date>.jsonl`,
|
|
8
|
+
* written by the OpenClaw EventLog and the Codex/WorkBuddy shared path via
|
|
9
|
+
* appendEventLogLine).
|
|
10
|
+
*
|
|
11
|
+
* Why this exists: SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 forbids the
|
|
12
|
+
* actual injection from running a dynamic budget while the Console forecasts
|
|
13
|
+
* under a different fixed 2000-char budget. The console holds no live host
|
|
14
|
+
* facts, but the RUNTIME persists the resolution it actually used —
|
|
15
|
+
* `runtime_v2_prompt_activations_injected` carries budgetChars, capacity
|
|
16
|
+
* provenance and hostKind. This module reads those facts back so console
|
|
17
|
+
* surfaces forecast under the same budget the last real injection used.
|
|
18
|
+
*
|
|
19
|
+
* Honesty rules this model enforces (SPEC §6.1/§7):
|
|
20
|
+
* - An observation is a FACT ABOUT THE PAST, never a live claim: callers must
|
|
21
|
+
* surface `observedAt`; the console labels it "most recent runtime
|
|
22
|
+
* observation", not a current confirmation.
|
|
23
|
+
* - No logs / no budget facts ⇒ absent, never a fabricated window; the caller
|
|
24
|
+
* keeps the honest fixed-fallback policy.
|
|
25
|
+
* - A latest observation older than RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS is
|
|
26
|
+
* `stale` — the caller must NOT present it as the current budget.
|
|
27
|
+
* - Observations bind to ONE host: OpenClaw's window is never used for a
|
|
28
|
+
* Codex or WorkBuddy forecast.
|
|
29
|
+
* - Untrusted input discipline: every line is parsed as `unknown` and
|
|
30
|
+
* narrowed with typeof guards + closed-set checks; structurally invalid
|
|
31
|
+
* lines are counted in `parseFailures` and skipped loudly, never guessed.
|
|
32
|
+
* - Events predating the model-aware policy carry no provenance fields; they
|
|
33
|
+
* are counted as `legacyEventsSkipped` and produce NO observation — a
|
|
34
|
+
* missing provenance field is "policy unknown", never a capacity claim.
|
|
35
|
+
* - Bounded I/O: only the newest bytes of the most recent daily files are read
|
|
36
|
+
* (tail window), the file cap applying PER candidate log directory, so a
|
|
37
|
+
* runaway or crafted log cannot make a Console request read an unbounded
|
|
38
|
+
* amount of data. Only bounded budget metadata is retained and returned;
|
|
39
|
+
* other event payloads are not cached.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Candidate directories for the runtime-v2 event log, in priority order.
|
|
43
|
+
* Shared with the RuleCode telemetry collector (ERR-031: both readers derive
|
|
44
|
+
* from one list — the v1 EventLog writer emits `.state/logs`, no production
|
|
45
|
+
* code ever created `.pd/logs`).
|
|
46
|
+
*/
|
|
47
|
+
export const RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS = ['.pd/logs', '.state/logs'];
|
|
48
|
+
/**
|
|
49
|
+
* Bounded I/O: only the most recent daily files are scanned — PER candidate
|
|
50
|
+
* log directory (`RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`), not globally. The
|
|
51
|
+
* cap is deliberately per directory: both directories describe the same
|
|
52
|
+
* workspace's log convention and either can hold the newest event for a given
|
|
53
|
+
* host (the reader takes latest-wins ACROSS them), so a global cap would let
|
|
54
|
+
* one directory's files starve the other and drop a live host observation.
|
|
55
|
+
* Worst case per read is therefore `RUNTIME_BUDGET_OBSERVATION_MAX_FILES ×
|
|
56
|
+
* RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS.length` files (14 today) and the same
|
|
57
|
+
* count × `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` bytes (56 MiB today).
|
|
58
|
+
*/
|
|
59
|
+
export const RUNTIME_BUDGET_OBSERVATION_MAX_FILES = 7;
|
|
60
|
+
/**
|
|
61
|
+
* Bounded I/O: per file, only the newest bytes are read. The writer appends,
|
|
62
|
+
* so the latest observation per host lives at the TAIL; a file larger than
|
|
63
|
+
* this window is tail-read and its older prefix skipped (counted in
|
|
64
|
+
* `truncatedFiles`) instead of materialising an unbounded log in the console
|
|
65
|
+
* process. 4 MiB is ~3.4x the largest real day observed on 2026-10-21
|
|
66
|
+
* (WorkBuddy 1.17 MiB, Codex 0.87 MiB, OpenClaw 0.98 MiB across 7 files).
|
|
67
|
+
*/
|
|
68
|
+
export const RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
69
|
+
/**
|
|
70
|
+
* Maximum age of an observation that may be presented as a CURRENT runtime
|
|
71
|
+
* fact. The runtime refreshes the observation on every prompt build, so an
|
|
72
|
+
* active workspace holds minute-old facts; beyond a day the model may have
|
|
73
|
+
* been switched and the window is no longer evidence about the present.
|
|
74
|
+
*/
|
|
75
|
+
export const RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
76
|
+
/**
|
|
77
|
+
* Clock-skew allowance for event timestamps. A writer clock ahead of ours is
|
|
78
|
+
* normal; an observation further in the future is not a fact about the
|
|
79
|
+
* present and must never be selected as the current budget.
|
|
80
|
+
*/
|
|
81
|
+
export const RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
82
|
+
/** Upper bound on the envelope `ts` string; anything longer is not a timestamp. */
|
|
83
|
+
const OBSERVED_AT_MAX_LENGTH = 64;
|
|
84
|
+
/** Console-side bounded reason: a runtime observation exists but is too old. */
|
|
85
|
+
export const RUNTIME_BUDGET_OBSERVATION_STALE_REASON = 'runtime_budget_observation_stale';
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
function nonEmptyString(value) {
|
|
90
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Windows paths are case-insensitive; compare them the way the OS does. */
|
|
93
|
+
function sameWorkspacePath(left, right) {
|
|
94
|
+
const resolvedLeft = path.resolve(left);
|
|
95
|
+
const resolvedRight = path.resolve(right);
|
|
96
|
+
return process.platform === 'win32'
|
|
97
|
+
? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
|
|
98
|
+
: resolvedLeft === resolvedRight;
|
|
99
|
+
}
|
|
100
|
+
function parseObservedAt(value) {
|
|
101
|
+
if (value.length > OBSERVED_AT_MAX_LENGTH)
|
|
102
|
+
return null;
|
|
103
|
+
const parsed = Date.parse(value);
|
|
104
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
105
|
+
}
|
|
106
|
+
const BUDGET_CAPACITY_SOURCES = new Set([
|
|
107
|
+
'host_effective_window',
|
|
108
|
+
'pi_catalog_exact',
|
|
109
|
+
'legacy_fallback',
|
|
110
|
+
]);
|
|
111
|
+
const BUDGET_CAPACITY_STATUSES = new Set(['confirmed', 'unknown']);
|
|
112
|
+
const BUDGET_FALLBACK_REASONS = new Set([
|
|
113
|
+
'host_capacity_absent',
|
|
114
|
+
'host_capacity_source_untrusted',
|
|
115
|
+
'host_capacity_invalid_value',
|
|
116
|
+
'catalog_unavailable',
|
|
117
|
+
'model_identity_absent',
|
|
118
|
+
'catalog_no_exact_match',
|
|
119
|
+
'catalog_no_match_for_model_id',
|
|
120
|
+
'catalog_ambiguous_model_id',
|
|
121
|
+
'catalog_provider_absent',
|
|
122
|
+
'catalog_metadata_invalid',
|
|
123
|
+
]);
|
|
124
|
+
function isBudgetFallbackReason(value) {
|
|
125
|
+
return typeof value === 'string' && BUDGET_FALLBACK_REASONS.has(value);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Narrow one untrusted event line into an observation, or null when it does
|
|
129
|
+
* not carry a trustworthy model-aware budget fact. rc-1/rc-2: every field is
|
|
130
|
+
* guarded; a structurally valid event missing provenance is NOT an
|
|
131
|
+
* observation (see `legacyEventsSkipped` at the report level).
|
|
132
|
+
*/
|
|
133
|
+
function narrowObservation(parsed, workspaceDir, nowMs) {
|
|
134
|
+
if (!isRecord(parsed))
|
|
135
|
+
return null;
|
|
136
|
+
if (nonEmptyString(parsed.type) !== 'runtime_v2_prompt_activations_injected')
|
|
137
|
+
return null;
|
|
138
|
+
const observedAt = nonEmptyString(parsed.ts);
|
|
139
|
+
const { data } = parsed;
|
|
140
|
+
if (observedAt === undefined || !isRecord(data))
|
|
141
|
+
return null;
|
|
142
|
+
const observedAtMs = parseObservedAt(observedAt);
|
|
143
|
+
if (observedAtMs === null)
|
|
144
|
+
return null;
|
|
145
|
+
// A timestamp beyond minor clock skew is not a fact about the present; it
|
|
146
|
+
// must not win the "latest observation" race or back a current forecast.
|
|
147
|
+
if (observedAtMs - nowMs > RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS)
|
|
148
|
+
return null;
|
|
149
|
+
// Workspace binding: the console model is workspace-scoped, so an event
|
|
150
|
+
// recorded under a different workspaceDir is not this workspace's fact.
|
|
151
|
+
const eventWorkspaceDir = nonEmptyString(data.workspaceDir);
|
|
152
|
+
if (eventWorkspaceDir === undefined || !sameWorkspacePath(eventWorkspaceDir, workspaceDir))
|
|
153
|
+
return null;
|
|
154
|
+
const rawHostKind = data.hostKind;
|
|
155
|
+
if (!isPromptInjectionTargetHost(rawHostKind))
|
|
156
|
+
return null;
|
|
157
|
+
const { budget } = data;
|
|
158
|
+
if (typeof budget !== 'number' || !Number.isInteger(budget) || budget <= 0)
|
|
159
|
+
return null;
|
|
160
|
+
// A budget computed under another policy version is not evidence about the
|
|
161
|
+
// CURRENT formula. Absent provenance is pre-policy, not "current by
|
|
162
|
+
// default": both are skipped, never reinterpreted.
|
|
163
|
+
if (data.budgetPolicyVersion !== PROMPT_BUDGET_POLICY_VERSION)
|
|
164
|
+
return null;
|
|
165
|
+
const rawSource = data.budgetCapacitySource;
|
|
166
|
+
const rawStatus = data.budgetCapacityStatus;
|
|
167
|
+
if (typeof rawSource !== 'string' || !BUDGET_CAPACITY_SOURCES.has(rawSource))
|
|
168
|
+
return null;
|
|
169
|
+
if (typeof rawStatus !== 'string' || !BUDGET_CAPACITY_STATUSES.has(rawStatus))
|
|
170
|
+
return null;
|
|
171
|
+
const rawWindow = data.budgetContextWindowTokens;
|
|
172
|
+
const rawReason = data.budgetFallbackReason;
|
|
173
|
+
if (rawReason !== undefined && !isBudgetFallbackReason(rawReason))
|
|
174
|
+
return null;
|
|
175
|
+
// Source/status/window/budget must describe ONE coherent resolution: a
|
|
176
|
+
// confirmed source needs a real window and exactly the budget the shared
|
|
177
|
+
// formula derives from it; the legacy fallback needs the legacy budget, no
|
|
178
|
+
// window, and a bounded reason. Anything else is not a trustworthy fact.
|
|
179
|
+
if (rawStatus === 'confirmed') {
|
|
180
|
+
if (rawSource !== 'host_effective_window' && rawSource !== 'pi_catalog_exact')
|
|
181
|
+
return null;
|
|
182
|
+
if (!isValidContextWindowTokens(rawWindow))
|
|
183
|
+
return null;
|
|
184
|
+
if (budget !== promptBudgetCharsForContextWindow(rawWindow))
|
|
185
|
+
return null;
|
|
186
|
+
if (rawReason !== undefined)
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
if (rawSource !== 'legacy_fallback')
|
|
191
|
+
return null;
|
|
192
|
+
if (rawWindow !== undefined)
|
|
193
|
+
return null;
|
|
194
|
+
if (!isBudgetFallbackReason(rawReason))
|
|
195
|
+
return null;
|
|
196
|
+
if (budget !== RUNTIME_V2_PRINCIPLE_BUDGET)
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
workspaceDir,
|
|
201
|
+
hostKind: rawHostKind,
|
|
202
|
+
budgetChars: budget,
|
|
203
|
+
capacitySource: rawSource,
|
|
204
|
+
capacityStatus: rawStatus,
|
|
205
|
+
contextWindowTokens: rawWindow ?? null,
|
|
206
|
+
fallbackReason: rawReason ?? null,
|
|
207
|
+
policyVersion: PROMPT_BUDGET_POLICY_VERSION,
|
|
208
|
+
observedAt,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Read at most the newest `RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES` of a
|
|
213
|
+
* log file. When the prefix is skipped, the partial first line after the cut
|
|
214
|
+
* is dropped so a half-written JSON line can never be parsed. Returns null
|
|
215
|
+
* when the file cannot be opened/read (the caller skips it loudly elsewhere).
|
|
216
|
+
*/
|
|
217
|
+
function readBoundedLogText(filePath) {
|
|
218
|
+
let fd;
|
|
219
|
+
try {
|
|
220
|
+
fd = fs.openSync(filePath, 'r');
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const { size } = fs.fstatSync(fd);
|
|
227
|
+
const start = Math.max(0, size - RUNTIME_BUDGET_OBSERVATION_MAX_FILE_BYTES);
|
|
228
|
+
const length = size - start;
|
|
229
|
+
if (length <= 0)
|
|
230
|
+
return { text: '', truncated: false };
|
|
231
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
232
|
+
let offset = 0;
|
|
233
|
+
while (offset < length) {
|
|
234
|
+
const read = fs.readSync(fd, buffer, offset, length - offset, start + offset);
|
|
235
|
+
if (read <= 0)
|
|
236
|
+
break;
|
|
237
|
+
offset += read;
|
|
238
|
+
}
|
|
239
|
+
let text = buffer.subarray(0, offset).toString('utf8');
|
|
240
|
+
if (start > 0) {
|
|
241
|
+
const newline = text.indexOf('\n');
|
|
242
|
+
text = newline === -1 ? '' : text.slice(newline + 1);
|
|
243
|
+
}
|
|
244
|
+
return { text, truncated: start > 0 };
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
try {
|
|
251
|
+
fs.closeSync(fd);
|
|
252
|
+
}
|
|
253
|
+
catch { /* fd already closed */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Read the most recent budget observation per host from the workspace's
|
|
258
|
+
* runtime-v2 event logs. Never throws: an unreadable log directory degrades
|
|
259
|
+
* to an empty report with an explicit note (absence is UNKNOWN, not zero).
|
|
260
|
+
*/
|
|
261
|
+
export function readRuntimePromptBudgetObservations(input) {
|
|
262
|
+
const { workspaceDir } = input;
|
|
263
|
+
const nowMs = (input.now ?? new Date()).getTime();
|
|
264
|
+
const filesRead = [];
|
|
265
|
+
const lines = { count: 0 };
|
|
266
|
+
const parseFailures = { count: 0 };
|
|
267
|
+
const legacyEventsSkipped = { count: 0 };
|
|
268
|
+
const foreignPolicyEventsSkipped = { count: 0 };
|
|
269
|
+
const truncatedFiles = { count: 0 };
|
|
270
|
+
const latest = new Map();
|
|
271
|
+
for (const candidate of RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS) {
|
|
272
|
+
const logsRoot = path.resolve(workspaceDir, ...candidate.split('/'));
|
|
273
|
+
if (!fs.existsSync(logsRoot))
|
|
274
|
+
continue;
|
|
275
|
+
let canonicalRoot;
|
|
276
|
+
try {
|
|
277
|
+
canonicalRoot = fs.realpathSync(logsRoot);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let names;
|
|
283
|
+
try {
|
|
284
|
+
names = fs.readdirSync(logsRoot)
|
|
285
|
+
.filter((name) => name.startsWith('events_') && name.endsWith('.jsonl'))
|
|
286
|
+
.sort()
|
|
287
|
+
.slice(-RUNTIME_BUDGET_OBSERVATION_MAX_FILES);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Unreadable directory contributes nothing; the note stays explicit.
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
for (const name of names) {
|
|
294
|
+
// Containment guard: only bare event filenames inside the resolved
|
|
295
|
+
// logs root are ever read — a crafted name cannot escape the directory.
|
|
296
|
+
// Canonicalize BOTH sides so an in-logs symlink cannot redirect the
|
|
297
|
+
// read outside the workspace log directory (EP-08).
|
|
298
|
+
const filePath = path.resolve(logsRoot, name);
|
|
299
|
+
if (!filePath.startsWith(logsRoot + path.sep) || path.basename(name) !== name)
|
|
300
|
+
continue;
|
|
301
|
+
let canonicalFile;
|
|
302
|
+
try {
|
|
303
|
+
canonicalFile = fs.realpathSync(filePath);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (!canonicalFile.startsWith(canonicalRoot + path.sep))
|
|
309
|
+
continue;
|
|
310
|
+
filesRead.push(canonicalFile);
|
|
311
|
+
const bounded = readBoundedLogText(canonicalFile);
|
|
312
|
+
if (bounded === null)
|
|
313
|
+
continue;
|
|
314
|
+
if (bounded.truncated)
|
|
315
|
+
truncatedFiles.count += 1;
|
|
316
|
+
for (const line of bounded.text.split('\n')) {
|
|
317
|
+
if (line.trim().length === 0)
|
|
318
|
+
continue;
|
|
319
|
+
lines.count += 1;
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(line);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
parseFailures.count += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (isRecord(parsed) && parsed.type === 'runtime_v2_prompt_activations_injected' && isRecord(parsed.data)) {
|
|
329
|
+
const { data } = parsed;
|
|
330
|
+
const rawPolicyVersion = data.budgetPolicyVersion;
|
|
331
|
+
if (rawPolicyVersion !== undefined && rawPolicyVersion !== PROMPT_BUDGET_POLICY_VERSION) {
|
|
332
|
+
// A budget computed under a DIFFERENT policy version is not
|
|
333
|
+
// evidence about the current formula — counted, not reinterpreted.
|
|
334
|
+
foreignPolicyEventsSkipped.count += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const observation = narrowObservation(parsed, workspaceDir, nowMs);
|
|
338
|
+
if (observation === null) {
|
|
339
|
+
// A pre-policy event, a future/invalid timestamp, incoherent
|
|
340
|
+
// provenance or an unusable binding — counted, never guessed.
|
|
341
|
+
legacyEventsSkipped.count += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const previous = latest.get(observation.hostKind);
|
|
345
|
+
// Compare PARSED instants, not strings: a longer/invalid timestamp
|
|
346
|
+
// must not shadow a real observation lexicographically.
|
|
347
|
+
if (previous === undefined || Date.parse(observation.observedAt) > Date.parse(previous.observedAt)) {
|
|
348
|
+
latest.set(observation.hostKind, observation);
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (isRecord(parsed) && parsed.type === 'runtime_v2_prompt_activations_injected' && !isRecord(parsed.data)) {
|
|
353
|
+
parseFailures.count += 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
observations: [...latest.values()],
|
|
360
|
+
filesRead,
|
|
361
|
+
lines: lines.count,
|
|
362
|
+
parseFailures: parseFailures.count,
|
|
363
|
+
legacyEventsSkipped: legacyEventsSkipped.count,
|
|
364
|
+
foreignPolicyEventsSkipped: foreignPolicyEventsSkipped.count,
|
|
365
|
+
truncatedFiles: truncatedFiles.count,
|
|
366
|
+
note: 'observations are PAST runtime facts bound to one host; absence means UNKNOWN (never a fabricated window); events without model-aware budget provenance, or computed under a different policy version, are skipped, not reinterpreted; the bounded read takes the newest daily files per candidate log directory and files larger than the tail window are counted in truncatedFiles (older prefix skipped, newest event per host still inside the window)',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** Latest-wins ordering that treats unparseable timestamps as older, not newer. */
|
|
370
|
+
function compareObservedAt(left, right) {
|
|
371
|
+
const leftMs = Date.parse(left);
|
|
372
|
+
const rightMs = Date.parse(right);
|
|
373
|
+
if (!Number.isFinite(leftMs))
|
|
374
|
+
return Number.isFinite(rightMs) ? -1 : 0;
|
|
375
|
+
if (!Number.isFinite(rightMs))
|
|
376
|
+
return 1;
|
|
377
|
+
return leftMs - rightMs;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Select the observation that may back a console forecast for `targetHost`.
|
|
381
|
+
* `absent` when the host has no observation at all; `stale` when the latest
|
|
382
|
+
* one is older than the freshness bound (the caller must fall back honestly).
|
|
383
|
+
*/
|
|
384
|
+
export function selectRuntimeBudgetObservation(input) {
|
|
385
|
+
const now = input.now ?? new Date();
|
|
386
|
+
const candidates = input.targetHost === undefined
|
|
387
|
+
? [...input.observations]
|
|
388
|
+
: input.observations.filter((observation) => observation.hostKind === input.targetHost);
|
|
389
|
+
let latest;
|
|
390
|
+
for (const candidate of candidates) {
|
|
391
|
+
if (latest === undefined || compareObservedAt(candidate.observedAt, latest.observedAt) > 0)
|
|
392
|
+
latest = candidate;
|
|
393
|
+
}
|
|
394
|
+
if (latest === undefined)
|
|
395
|
+
return { kind: 'absent' };
|
|
396
|
+
const observedAt = Date.parse(latest.observedAt);
|
|
397
|
+
if (!Number.isFinite(observedAt)
|
|
398
|
+
|| observedAt - now.getTime() > RUNTIME_BUDGET_OBSERVATION_MAX_FUTURE_SKEW_MS
|
|
399
|
+
|| now.getTime() - observedAt > RUNTIME_BUDGET_OBSERVATION_MAX_AGE_MS) {
|
|
400
|
+
return { kind: 'stale', observation: latest };
|
|
401
|
+
}
|
|
402
|
+
return { kind: 'usable', observation: latest };
|
|
403
|
+
}
|
|
404
|
+
export function selectConsoleRuntimeBudgetFacts(input) {
|
|
405
|
+
const report = readRuntimePromptBudgetObservations({ workspaceDir: input.workspaceDir, now: input.now });
|
|
406
|
+
const perHost = new Map();
|
|
407
|
+
const stalePerHost = new Map();
|
|
408
|
+
for (const observation of report.observations) {
|
|
409
|
+
const selection = selectRuntimeBudgetObservation({
|
|
410
|
+
observations: report.observations,
|
|
411
|
+
targetHost: observation.hostKind,
|
|
412
|
+
now: input.now,
|
|
413
|
+
});
|
|
414
|
+
if (selection.kind === 'usable')
|
|
415
|
+
perHost.set(observation.hostKind, selection.observation);
|
|
416
|
+
else if (selection.kind === 'stale')
|
|
417
|
+
stalePerHost.set(observation.hostKind, selection.observation);
|
|
418
|
+
}
|
|
419
|
+
if (input.targetHost === undefined) {
|
|
420
|
+
return { perHost, stalePerHost };
|
|
421
|
+
}
|
|
422
|
+
const selection = selectRuntimeBudgetObservation({
|
|
423
|
+
observations: report.observations,
|
|
424
|
+
targetHost: input.targetHost,
|
|
425
|
+
now: input.now,
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
...(selection.kind === 'usable' ? { selected: selection.observation } : {}),
|
|
429
|
+
...(selection.kind === 'stale' ? { stale: selection.observation } : {}),
|
|
430
|
+
perHost,
|
|
431
|
+
stalePerHost,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Rebuild the budget resolution an observation stands for, so console
|
|
436
|
+
* projections run under the SAME resolution shape the real injection used.
|
|
437
|
+
* `fallbackDetail` is deliberately dropped: it is log-only context and never
|
|
438
|
+
* rides events or console payloads.
|
|
439
|
+
*/
|
|
440
|
+
export function runtimeBudgetObservationToResolution(observation) {
|
|
441
|
+
return {
|
|
442
|
+
budgetChars: observation.budgetChars,
|
|
443
|
+
capacitySource: observation.capacitySource,
|
|
444
|
+
capacityStatus: observation.capacityStatus,
|
|
445
|
+
contextWindowTokens: observation.contextWindowTokens,
|
|
446
|
+
policyVersion: observation.policyVersion ?? PROMPT_BUDGET_POLICY_VERSION,
|
|
447
|
+
...(observation.fallbackReason !== null ? { fallbackReason: observation.fallbackReason } : {}),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
@@ -232,4 +232,20 @@ export declare function resolveLivePromptInjectionProjection(input: {
|
|
|
232
232
|
targetHost?: PromptInjectionTargetHost;
|
|
233
233
|
excludePrincipleIds?: ReadonlySet<string>;
|
|
234
234
|
roundKey?: number;
|
|
235
|
+
/**
|
|
236
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 / PRI-1017: budget resolution
|
|
237
|
+
* the confirmed projection must run under. Console callers pass the
|
|
238
|
+
* resolution rebuilt from the workspace's most recent runtime budget
|
|
239
|
+
* OBSERVATION (`runtimeBudgetObservationToResolution`) so the forecast uses
|
|
240
|
+
* the same budget the last real injection used. Absent → the fixed-fallback
|
|
241
|
+
* default (the honest "no runtime capacity confirmed" policy).
|
|
242
|
+
*/
|
|
243
|
+
promptBudget?: PromptBudgetResolution;
|
|
244
|
+
/**
|
|
245
|
+
* PRI-1017: per-host budget resolutions for the unconfirmed per-host
|
|
246
|
+
* forecasts. Each host's projection runs under THAT host's observation —
|
|
247
|
+
* never another host's window. Absent per host → that host's honest
|
|
248
|
+
* fixed-fallback default.
|
|
249
|
+
*/
|
|
250
|
+
perHostPromptBudget?: Partial<Record<PromptInjectionTargetHost, PromptBudgetResolution>>;
|
|
235
251
|
}): Promise<LivePromptInjectionProjectionResolution>;
|
|
@@ -334,20 +334,35 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
334
334
|
};
|
|
335
335
|
}
|
|
336
336
|
export async function resolveLivePromptInjectionProjection(input) {
|
|
337
|
-
const
|
|
337
|
+
const { promptBudget, perHostPromptBudget, ...projectionInput } = input;
|
|
338
|
+
const decision = resolvePromptInjectionRouteDecision(projectionInput);
|
|
338
339
|
if (decision.status === 'confirmed') {
|
|
340
|
+
// PRI-1017: the route can be confirmed by WORKSPACE FACTS without any
|
|
341
|
+
// request-level targetHost (a single-host workspace). The caller then has
|
|
342
|
+
// no `promptBudget`, but it does have that host's own observation in the
|
|
343
|
+
// per-host map — use it. A flag-shared route with no hostKind stays on
|
|
344
|
+
// the honest fallback: no host was identified, so no host's window may
|
|
345
|
+
// be borrowed.
|
|
346
|
+
const decidedBudget = promptBudget
|
|
347
|
+
?? (decision.hostKind !== undefined ? perHostPromptBudget?.[decision.hostKind] : undefined);
|
|
339
348
|
const projection = await buildLivePromptInjectionProjection({
|
|
340
|
-
...
|
|
349
|
+
...projectionInput,
|
|
341
350
|
targetHost: decision.hostKind,
|
|
351
|
+
...(decidedBudget !== undefined ? { promptBudget: decidedBudget } : {}),
|
|
342
352
|
});
|
|
343
353
|
return { status: 'confirmed', decision, projection };
|
|
344
354
|
}
|
|
345
355
|
const hosts = decision.perHost?.map((entry) => entry.hostKind) ?? [...PROMPT_INJECTION_TARGET_HOSTS];
|
|
346
356
|
const perHost = [];
|
|
347
357
|
for (const hostKind of hosts) {
|
|
358
|
+
const hostBudget = perHostPromptBudget?.[hostKind];
|
|
348
359
|
perHost.push({
|
|
349
360
|
hostKind,
|
|
350
|
-
projection: await buildLivePromptInjectionProjection({
|
|
361
|
+
projection: await buildLivePromptInjectionProjection({
|
|
362
|
+
...projectionInput,
|
|
363
|
+
targetHost: hostKind,
|
|
364
|
+
...(hostBudget !== undefined ? { promptBudget: hostBudget } : {}),
|
|
365
|
+
}),
|
|
351
366
|
});
|
|
352
367
|
}
|
|
353
368
|
return { status: 'unconfirmed', decision, perHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PromptBudgetTierFit } from '@principles/core/runtime-v2';
|
|
1
|
+
import { type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution, type PromptBudgetTierFit } from '@principles/core/runtime-v2';
|
|
2
2
|
import { type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
3
|
/**
|
|
4
4
|
* SPEC PD_PROMPT_CAPACITY_V1 R-A3: per-live-activation injection status for
|
|
@@ -45,6 +45,18 @@ export interface PromptActivationInjectionStatus {
|
|
|
45
45
|
tierFit: PromptBudgetTierFit;
|
|
46
46
|
}[];
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* PRI-1017: provenance of the budget the rows were measured against, copied
|
|
50
|
+
* from the SAME resolution the projection ran under. Optional on every field
|
|
51
|
+
* so a caller can degrade without inventing a capacity fact.
|
|
52
|
+
*/
|
|
53
|
+
export interface PromptActivationBudgetProvenance {
|
|
54
|
+
capacitySource?: PromptBudgetCapacitySource;
|
|
55
|
+
capacityStatus?: PromptBudgetCapacityStatus;
|
|
56
|
+
contextWindowTokens?: number;
|
|
57
|
+
policyVersion?: string;
|
|
58
|
+
fallbackReason?: PromptBudgetFallbackReason;
|
|
59
|
+
}
|
|
48
60
|
export interface PromptActivationInjectionStatusReport {
|
|
49
61
|
route: PromptInjectionRouteDecision;
|
|
50
62
|
/** Empty when injection is globally unavailable (prompt flag off / state.db missing) — see `globalReason`. */
|
|
@@ -52,8 +64,23 @@ export interface PromptActivationInjectionStatusReport {
|
|
|
52
64
|
warnings: string[];
|
|
53
65
|
/** Present when no status could be computed at all (fail-loud degradation, rc-9). */
|
|
54
66
|
globalReason?: string;
|
|
67
|
+
/** Present for a confirmed route: the budget basis every row was measured under. */
|
|
68
|
+
budgetProvenance?: PromptActivationBudgetProvenance;
|
|
55
69
|
}
|
|
56
70
|
export declare function readPromptActivationInjectionStatuses(input: {
|
|
57
71
|
workspaceDir: string;
|
|
58
72
|
targetHost?: PromptInjectionTargetHost;
|
|
73
|
+
/**
|
|
74
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.1 / PRI-1017: budget resolution
|
|
75
|
+
* the status projection must run under — console callers pass the
|
|
76
|
+
* resolution rebuilt from the workspace's most recent runtime budget
|
|
77
|
+
* observation. Absent → the honest fixed-fallback default.
|
|
78
|
+
*/
|
|
79
|
+
promptBudget?: PromptBudgetResolution;
|
|
80
|
+
/**
|
|
81
|
+
* PRI-1017: per-host observations for a route confirmed by WORKSPACE FACTS
|
|
82
|
+
* (no request-level targetHost). The projection uses only the resolved
|
|
83
|
+
* host's own budget; an unidentified shared route stays on the fallback.
|
|
84
|
+
*/
|
|
85
|
+
perHostPromptBudget?: Partial<Record<PromptInjectionTargetHost, PromptBudgetResolution>>;
|
|
59
86
|
}): Promise<PromptActivationInjectionStatusReport>;
|
|
@@ -103,9 +103,12 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
103
103
|
});
|
|
104
104
|
return { route, statuses, warnings };
|
|
105
105
|
}
|
|
106
|
+
const decidedBudget = input.promptBudget
|
|
107
|
+
?? (route.hostKind !== undefined ? input.perHostPromptBudget?.[route.hostKind] : undefined);
|
|
106
108
|
const projection = await buildLivePromptInjectionProjection({
|
|
107
109
|
workspaceDir: input.workspaceDir,
|
|
108
110
|
targetHost: route.hostKind,
|
|
111
|
+
...(decidedBudget !== undefined ? { promptBudget: decidedBudget } : {}),
|
|
109
112
|
});
|
|
110
113
|
warnings.push(...projection.warnings);
|
|
111
114
|
const statuses = rows.map((row) => {
|
|
@@ -122,12 +125,12 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
122
125
|
const budgetScope = projection.budgetScope;
|
|
123
126
|
const serialization = projection.route === 'shared_render' ? 'full directive render' : 'list entry';
|
|
124
127
|
// Tier verdict FIRST (SPEC §6.3): the projection budget on this surface
|
|
125
|
-
// is
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
128
|
+
// is the fixed fallback (2000) unless the caller passed a runtime budget
|
|
129
|
+
// OBSERVATION (PRI-1017) — and even then an entry in (MIN, fallback]
|
|
130
|
+
// exceeds the measured floor, so on a 32K/64K model (budget = floor) it
|
|
131
|
+
// would NOT inject. Reporting it as an unconditional `injectable` would
|
|
132
|
+
// contradict the approval-time `model_dependent` verdict for the very
|
|
133
|
+
// same artifact.
|
|
131
134
|
const tierFit = classifySingleItemBudgetTier(costChars);
|
|
132
135
|
if (tierFit === 'exceeds_max_tier') {
|
|
133
136
|
return {
|
|
@@ -166,5 +169,16 @@ export async function readPromptActivationInjectionStatuses(input) {
|
|
|
166
169
|
route: projection.route,
|
|
167
170
|
};
|
|
168
171
|
});
|
|
169
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
route,
|
|
174
|
+
statuses,
|
|
175
|
+
warnings,
|
|
176
|
+
budgetProvenance: {
|
|
177
|
+
...(projection.budgetCapacitySource !== undefined ? { capacitySource: projection.budgetCapacitySource } : {}),
|
|
178
|
+
...(projection.budgetCapacityStatus !== undefined ? { capacityStatus: projection.budgetCapacityStatus } : {}),
|
|
179
|
+
...(typeof projection.budgetContextWindowTokens === 'number' ? { contextWindowTokens: projection.budgetContextWindowTokens } : {}),
|
|
180
|
+
...(projection.budgetPolicyVersion !== undefined ? { policyVersion: projection.budgetPolicyVersion } : {}),
|
|
181
|
+
...(projection.budgetFallbackReason !== undefined ? { fallbackReason: projection.budgetFallbackReason } : {}),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
170
184
|
}
|