create-principles-disciple 1.145.12 → 1.145.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/_release/product-identity.json +2 -2
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/models/ActivationsConsoleModel.d.ts +3 -1
- package/console/dist/server/models/ActivationsConsoleModel.js +38 -4
- package/console/dist/server/models/ApprovalsConsoleModel.js +56 -7
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +12 -0
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +70 -13
- package/console/dist/server/routes/approvals-grouped.js +5 -2
- package/console/dist/shared/prompt-injection-contract.d.ts +58 -5
- package/console/dist/ui/i18n/en.json +25 -1
- package/console/dist/ui/i18n/zh-CN.json +25 -1
- package/console/dist/ui/pages/activation/ActivationPage.js +39 -1
- package/console/dist/ui/pages/focus/FocusPage.js +70 -28
- package/console/dist/ui/utils/validators.js +70 -11
- package/console/dist/web/assets/app.js +24 -24
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +4 -0
- package/host-runtime/dist/prompt-injection-budget-observation.d.ts +176 -0
- package/host-runtime/dist/prompt-injection-budget-observation.js +449 -0
- package/host-runtime/dist/prompt-injection-projection.d.ts +16 -0
- package/host-runtime/dist/prompt-injection-projection.js +18 -3
- package/host-runtime/dist/prompt-injection-status.d.ts +28 -1
- package/host-runtime/dist/prompt-injection-status.js +21 -7
- package/host-runtime/package.json +1 -1
- package/package.json +1 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts +3 -1
- package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.js +5 -3
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +288 -288
- package/release-manager/package.json +1 -1
- package/workbuddy-adapter/package.json +1 -1
|
@@ -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';
|
|
@@ -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>;
|