create-principles-disciple 1.145.11 → 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/dist/codec/input-decoder.js +11 -3
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/models/ActivationsConsoleModel.d.ts +3 -1
- package/console/dist/server/models/ActivationsConsoleModel.js +39 -5
- package/console/dist/server/models/ApprovalsConsoleModel.d.ts +1 -1
- package/console/dist/server/models/ApprovalsConsoleModel.js +64 -8
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +25 -1
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +116 -24
- package/console/dist/server/routes/approvals-grouped.js +5 -2
- package/console/dist/shared/prompt-injection-contract.d.ts +83 -2
- 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 +56 -5
- package/console/dist/ui/pages/focus/FocusPage.js +87 -21
- package/console/dist/ui/pages/focus/focus-validation.js +12 -0
- package/console/dist/ui/utils/validators.d.ts +6 -0
- package/console/dist/ui/utils/validators.js +93 -4
- package/console/dist/web/assets/app.js +24 -24
- package/core/dist/host/host-adapter.d.ts +27 -0
- package/core/dist/host/host-adapter.d.ts.map +1 -1
- package/core/dist/host/host-adapter.js.map +1 -1
- package/core/dist/host/index.d.ts +1 -1
- package/core/dist/host/index.d.ts.map +1 -1
- package/core/dist/host/index.js.map +1 -1
- package/core/dist/runtime-v2/activation/index.d.ts +2 -0
- package/core/dist/runtime-v2/activation/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/index.js +2 -0
- package/core/dist/runtime-v2/activation/index.js.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts +106 -0
- package/core/dist/runtime-v2/activation/prompt-budget-policy.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/prompt-budget-policy.js +229 -0
- package/core/dist/runtime-v2/activation/prompt-budget-policy.js.map +1 -0
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.d.ts +2 -0
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.js +50 -1
- package/core/dist/runtime-v2/adapter/pi-ai-catalog.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +3 -3
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +2 -2
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +24 -0
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +22 -0
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/package.json +1 -1
- package/host-runtime/dist/active-principle-prompt.d.ts +23 -1
- package/host-runtime/dist/active-principle-prompt.js +21 -5
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +37 -2
- package/host-runtime/dist/prompt-activation-deliverability.d.ts +36 -3
- package/host-runtime/dist/prompt-activation-deliverability.js +44 -26
- 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 +39 -1
- package/host-runtime/dist/prompt-injection-projection.js +43 -9
- package/host-runtime/dist/prompt-injection-status.d.ts +47 -5
- package/host-runtime/dist/prompt-injection-status.js +45 -8
- package/host-runtime/package.json +1 -1
- package/package.json +2 -2
- 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 +23 -6
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +430 -430
- package/plugin/dist/governance-audit.js +2 -2
- package/plugin/dist/openclaw-sdk.d.ts +9 -1
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +2 -2
- package/workbuddy-adapter/dist/codec/input-decoder.js +16 -4
- package/workbuddy-adapter/package.json +1 -1
|
@@ -59,10 +59,15 @@ function validateCommon(raw, needsTurn) {
|
|
|
59
59
|
const sessionId = requiredString(raw, 'session_id');
|
|
60
60
|
requiredNullableString(raw, 'transcript_path');
|
|
61
61
|
const workspaceDir = requiredString(raw, 'cwd');
|
|
62
|
-
|
|
62
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: keep the VALIDATED model identity
|
|
63
|
+
// (was: checked then discarded). It is an identity string only — never a
|
|
64
|
+
// capacity; the budget resolver turns it into a budget via the installed
|
|
65
|
+
// catalog, falling back to the fixed budget on unknown/ambiguous ids. It
|
|
66
|
+
// must not block the hook or leak into tool allow/deny decisions.
|
|
67
|
+
const modelId = requiredString(raw, 'model');
|
|
63
68
|
requiredString(raw, 'permission_mode');
|
|
64
69
|
const turnId = needsTurn ? requiredString(raw, 'turn_id') : undefined;
|
|
65
|
-
return { workspaceDir, sessionId, ...(turnId ? { turnId } : {}) };
|
|
70
|
+
return { base: { workspaceDir, sessionId, ...(turnId ? { turnId } : {}) }, modelId };
|
|
66
71
|
}
|
|
67
72
|
export function decodeCodexInput(raw) {
|
|
68
73
|
if (!isRecord(raw))
|
|
@@ -71,7 +76,10 @@ export function decodeCodexInput(raw) {
|
|
|
71
76
|
const kind = EVENT_KINDS.get(eventName);
|
|
72
77
|
if (!kind)
|
|
73
78
|
throw new CodexDecoderError(`unknown hook_event_name "${eventName}"`, 'Configure only PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, or Stop.');
|
|
74
|
-
const
|
|
79
|
+
const { base, modelId } = validateCommon(raw, kind !== 'session_start');
|
|
80
|
+
// Model identity rides every event context under modelContext (budget
|
|
81
|
+
// resolution happens on the prompt path; other paths carry it harmlessly).
|
|
82
|
+
const common = { ...base, modelContext: { modelId } };
|
|
75
83
|
let context;
|
|
76
84
|
let rawPayload = raw;
|
|
77
85
|
if (kind === 'before_tool_call' || kind === 'after_tool_call') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/codex-adapter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -127,7 +127,9 @@ export interface OwnerMutationInput {
|
|
|
127
127
|
* that path made every activation's shadow metrics report "unavailable" in the
|
|
128
128
|
* console while real evaluations accumulated unread in `.state/logs`. Readers
|
|
129
129
|
* scan both candidates until the writer migrates (ERR-031: both readers derive
|
|
130
|
-
* from this same list
|
|
130
|
+
* from this same list — the single source is the host-runtime constant
|
|
131
|
+
* `RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`, shared with the PRI-1017 budget
|
|
132
|
+
* observation reader so the two readers can never drift).
|
|
131
133
|
*/
|
|
132
134
|
export declare const RULECODE_EVENT_LOG_CANDIDATE_DIRS: readonly string[];
|
|
133
135
|
export interface CollectedRuleCodeEventEntries {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SqliteConnection, SqliteActivationStateStore, SqlitePIArtifactStore, extractPrincipleId, extractEvidenceRefs, PromotionReadinessReader, RuleHostWriter, createProductionGateDeps, SqliteActivationSafetyStore, isFeatureEnabled, RuleCodeOwnerDecisionService, collectOpenClawPromotionChecks, summarizeRuleCodeShadowEvents, buildPromotionEvidenceSnapshot, } from '@principles/core/runtime-v2';
|
|
2
|
-
import { resolvePromotionHostLiveness, resolveWorkspaceHostToolSemantics, readPromptActivationInjectionStatuses } from '@principles/host-runtime';
|
|
2
|
+
import { resolvePromotionHostLiveness, resolveWorkspaceHostToolSemantics, readPromptActivationInjectionStatuses, runtimeBudgetObservationToResolution, selectConsoleRuntimeBudgetFacts, RUNTIME_BUDGET_OBSERVATION_STALE_REASON, RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS } from '@principles/host-runtime';
|
|
3
3
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
@@ -63,9 +63,11 @@ function unavailableShadowSummary() {
|
|
|
63
63
|
* that path made every activation's shadow metrics report "unavailable" in the
|
|
64
64
|
* console while real evaluations accumulated unread in `.state/logs`. Readers
|
|
65
65
|
* scan both candidates until the writer migrates (ERR-031: both readers derive
|
|
66
|
-
* from this same list
|
|
66
|
+
* from this same list — the single source is the host-runtime constant
|
|
67
|
+
* `RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS`, shared with the PRI-1017 budget
|
|
68
|
+
* observation reader so the two readers can never drift).
|
|
67
69
|
*/
|
|
68
|
-
export const RULECODE_EVENT_LOG_CANDIDATE_DIRS =
|
|
70
|
+
export const RULECODE_EVENT_LOG_CANDIDATE_DIRS = RUNTIME_V2_EVENT_LOG_CANDIDATE_DIRS;
|
|
69
71
|
/**
|
|
70
72
|
* Collect rulehost telemetry entries from all candidate log directories.
|
|
71
73
|
* Malformed lines are excluded individually. A directory counts as a source
|
|
@@ -318,10 +320,33 @@ export class ActivationsConsoleModel {
|
|
|
318
320
|
// status with positive single-item evidence. Degradation is observable
|
|
319
321
|
// (route summary carries the reason) and never turns a row "healthy"
|
|
320
322
|
// by omission (rc-9 / AC-06).
|
|
323
|
+
// PRI-1017: the status projection runs under the workspace's most
|
|
324
|
+
// recent runtime budget OBSERVATION for the target host, so per-row
|
|
325
|
+
// `budget` figures match the budget the last real injection used —
|
|
326
|
+
// SPEC §6.1's same-budget rule for this surface too.
|
|
321
327
|
let promptInjectionRoute;
|
|
322
328
|
let statusByActivation;
|
|
323
329
|
try {
|
|
324
|
-
const
|
|
330
|
+
const budgetFacts = selectConsoleRuntimeBudgetFacts({
|
|
331
|
+
workspaceDir: this.workspaceDir,
|
|
332
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
333
|
+
});
|
|
334
|
+
const perHostBudget = new Map([...budgetFacts.perHost].map(([hostKind, observation]) => [hostKind, runtimeBudgetObservationToResolution(observation)]));
|
|
335
|
+
const report = await readPromptActivationInjectionStatuses({
|
|
336
|
+
workspaceDir: this.workspaceDir,
|
|
337
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
338
|
+
...(budgetFacts.selected !== undefined
|
|
339
|
+
? { promptBudget: runtimeBudgetObservationToResolution(budgetFacts.selected) }
|
|
340
|
+
: {}),
|
|
341
|
+
...(perHostBudget.size > 0 ? { perHostPromptBudget: Object.fromEntries(perHostBudget) } : {}),
|
|
342
|
+
});
|
|
343
|
+
// The status report resolved the route internally; bind the observation
|
|
344
|
+
// to THAT host only. A stale observation is never presented as current.
|
|
345
|
+
const decidedHost = report.route.status === 'confirmed' ? report.route.hostKind : undefined;
|
|
346
|
+
const usedObservation = decidedHost !== undefined ? budgetFacts.perHost.get(decidedHost) : undefined;
|
|
347
|
+
const staleObservation = decidedHost !== undefined && usedObservation === undefined
|
|
348
|
+
? budgetFacts.stalePerHost.get(decidedHost)
|
|
349
|
+
: undefined;
|
|
325
350
|
promptInjectionRoute = {
|
|
326
351
|
status: report.route.status,
|
|
327
352
|
...(report.route.hostKind !== undefined ? { hostKind: report.route.hostKind } : {}),
|
|
@@ -330,6 +355,15 @@ export class ActivationsConsoleModel {
|
|
|
330
355
|
...(report.route.unconfirmedReason !== undefined ? { unconfirmedReason: report.route.unconfirmedReason } : {}),
|
|
331
356
|
...(report.route.nextAction !== undefined ? { nextAction: report.route.nextAction } : {}),
|
|
332
357
|
...(report.globalReason !== undefined ? { globalReason: report.globalReason } : {}),
|
|
358
|
+
...(report.budgetProvenance?.capacitySource !== undefined ? { budgetCapacitySource: report.budgetProvenance.capacitySource } : {}),
|
|
359
|
+
...(report.budgetProvenance?.capacityStatus !== undefined ? { budgetCapacityStatus: report.budgetProvenance.capacityStatus } : {}),
|
|
360
|
+
...(report.budgetProvenance?.contextWindowTokens !== undefined ? { budgetContextWindowTokens: report.budgetProvenance.contextWindowTokens } : {}),
|
|
361
|
+
...(staleObservation !== undefined
|
|
362
|
+
? { budgetFallbackReason: RUNTIME_BUDGET_OBSERVATION_STALE_REASON }
|
|
363
|
+
: report.budgetProvenance?.fallbackReason !== undefined
|
|
364
|
+
? { budgetFallbackReason: report.budgetProvenance.fallbackReason }
|
|
365
|
+
: {}),
|
|
366
|
+
...(usedObservation !== undefined ? { budgetObservedAt: usedObservation.observedAt } : {}),
|
|
333
367
|
};
|
|
334
368
|
statusByActivation = new Map(report.statuses.map((entry) => [
|
|
335
369
|
entry.activationId,
|
|
@@ -343,7 +377,7 @@ export class ActivationsConsoleModel {
|
|
|
343
377
|
...(entry.route !== undefined ? { route: entry.route } : {}),
|
|
344
378
|
...(entry.reason !== undefined ? { reason: entry.reason } : {}),
|
|
345
379
|
...(entry.perHostFits !== undefined
|
|
346
|
-
? { perHostFits: entry.perHostFits.map((fit) => ({ hostKind: fit.hostKind,
|
|
380
|
+
? { perHostFits: entry.perHostFits.map((fit) => ({ hostKind: fit.hostKind, tierFit: fit.tierFit })) }
|
|
347
381
|
: {}),
|
|
348
382
|
},
|
|
349
383
|
]));
|
|
@@ -3,7 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { SqliteConnection, SqliteApprovalQueueStore, SqliteActivationStateStore, SqlitePIArtifactStore, ActivationDispatcher, PromptWriter, DeferArchiveWriter, RuleHostWriter, createProductionGateDeps, ApprovalCompletionService, ApprovalQueue, mapConfidenceToLabel, isArtifactRevisionOf, detectPromptReplacementTarget, buildOwnerRevisionArtifact, PrincipleTreeLedgerAdapter, MVP_CHANNELS, } from '@principles/core/runtime-v2';
|
|
4
4
|
import { resolveLedgerPrincipleId } from './principle-id-resolution.js';
|
|
5
5
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
|
|
6
|
-
import { resolveWorkspaceHostToolSemantics, checkPromptArtifactDeliverabilityById, resolveLivePromptInjectionProjection, } from '@principles/host-runtime';
|
|
6
|
+
import { resolveWorkspaceHostToolSemantics, checkPromptArtifactDeliverabilityById, resolveLivePromptInjectionProjection, runtimeBudgetObservationToResolution, selectConsoleRuntimeBudgetFacts, } from '@principles/host-runtime';
|
|
7
7
|
const MVP_PROVEN_CHANNELS = new Set(MVP_CHANNELS);
|
|
8
8
|
const EMPTY_STATS = { pending: 0, approved: 0, rejected: 0, cancelled: 0 };
|
|
9
9
|
function isMissingTableError(err) {
|
|
@@ -14,6 +14,30 @@ function isMissingTableError(err) {
|
|
|
14
14
|
function isActivationSuccess(activation) {
|
|
15
15
|
return activation.decision === 'activated' || activation.decision === 'already_activated';
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* PRI-1017 (SPEC §6.5 rules 1/8): name the budget basis a forecast ACTUALLY
|
|
19
|
+
* ran under. Every case is spelled out — silence reads as "the console is not
|
|
20
|
+
* bound to the runtime", and on an unconfirmed route a per-host number
|
|
21
|
+
* without its own basis lets one host's observation look like another host's
|
|
22
|
+
* confirmation.
|
|
23
|
+
*/
|
|
24
|
+
function describeBudgetBasis(hostKind, facts) {
|
|
25
|
+
const used = facts.perHost.get(hostKind);
|
|
26
|
+
if (used !== undefined) {
|
|
27
|
+
return used.capacityStatus === 'confirmed'
|
|
28
|
+
? `budget basis=runtime_observation (${hostKind}, observed at ${used.observedAt})`
|
|
29
|
+
// A usable observation can itself be the runtime's honest fallback (the
|
|
30
|
+
// host supplied no trusted capacity). The number is then the fixed
|
|
31
|
+
// budget, not a host window — say so.
|
|
32
|
+
: `budget basis=runtime_observation_fallback (${hostKind}, runtime reason=${used.fallbackReason ?? 'unknown'}, observed at ${used.observedAt})`;
|
|
33
|
+
}
|
|
34
|
+
const stale = facts.stalePerHost.get(hostKind);
|
|
35
|
+
if (stale !== undefined) {
|
|
36
|
+
return `budget basis=fixed_fallback (the runtime budget observation for ${hostKind} is stale — older than 24h and NOT used as the current budget)`;
|
|
37
|
+
}
|
|
38
|
+
// No observation at all: the number is the console's fixed fallback.
|
|
39
|
+
return `budget basis=fixed_fallback (no runtime budget observation for ${hostKind})`;
|
|
40
|
+
}
|
|
17
41
|
function stateDbExists(workspaceDir) {
|
|
18
42
|
return fs.existsSync(path.join(workspaceDir, '.pd', 'state.db'));
|
|
19
43
|
}
|
|
@@ -252,6 +276,13 @@ export class ApprovalsConsoleModel {
|
|
|
252
276
|
}
|
|
253
277
|
if (deliverability.status === 'deliverable')
|
|
254
278
|
return undefined;
|
|
279
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3: model_dependent is NOT a
|
|
280
|
+
// refusal — the entry fits within the supported budget range and whether
|
|
281
|
+
// it injects depends on the runtime model. The approval proceeds; the
|
|
282
|
+
// honest uncertainty is already visible on the pre-approval badge
|
|
283
|
+
// (promptBudgetTierFit) and the activation status surface.
|
|
284
|
+
if (deliverability.status === 'model_dependent')
|
|
285
|
+
return undefined;
|
|
255
286
|
if (deliverability.status === 'undeliverable') {
|
|
256
287
|
return {
|
|
257
288
|
ok: false,
|
|
@@ -276,7 +307,7 @@ export class ApprovalsConsoleModel {
|
|
|
276
307
|
capacity: {
|
|
277
308
|
category: deliverability.reason.startsWith('route_unconfirmed') ? 'route_unconfirmed' : 'content_unconfirmed',
|
|
278
309
|
...(deliverability.perHost !== undefined
|
|
279
|
-
? { perHost: deliverability.perHost.map((entry) => ({ hostKind: entry.hostKind,
|
|
310
|
+
? { perHost: deliverability.perHost.map((entry) => ({ hostKind: entry.hostKind, tierFit: entry.tierFit, costChars: entry.costChars })) }
|
|
280
311
|
: {}),
|
|
281
312
|
},
|
|
282
313
|
};
|
|
@@ -295,30 +326,55 @@ export class ApprovalsConsoleModel {
|
|
|
295
326
|
*/
|
|
296
327
|
async checkPromptInjectionBudget(activationId, targetHost) {
|
|
297
328
|
let resolution;
|
|
329
|
+
let budgetFacts;
|
|
330
|
+
// PRI-1017: the post-approval forecast runs under the workspace's most
|
|
331
|
+
// recent runtime budget OBSERVATION for the target host (per-host for
|
|
332
|
+
// unconfirmed routes), so the warning describes the budget the last real
|
|
333
|
+
// injection used — SPEC §6.1's same-budget rule. The read model degrades
|
|
334
|
+
// to "no observation" on unreadable logs; a projection failure below stays
|
|
335
|
+
// a traceable warning because the activation is already committed.
|
|
298
336
|
try {
|
|
337
|
+
budgetFacts = selectConsoleRuntimeBudgetFacts({
|
|
338
|
+
workspaceDir: this.workspaceDir,
|
|
339
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
340
|
+
});
|
|
341
|
+
const perHostBudget = new Map([...budgetFacts.perHost].map(([hostKind, observation]) => [hostKind, runtimeBudgetObservationToResolution(observation)]));
|
|
299
342
|
resolution = await resolveLivePromptInjectionProjection({
|
|
300
343
|
workspaceDir: this.workspaceDir,
|
|
301
|
-
targetHost,
|
|
344
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
345
|
+
...(budgetFacts.selected !== undefined
|
|
346
|
+
? { promptBudget: runtimeBudgetObservationToResolution(budgetFacts.selected) }
|
|
347
|
+
: {}),
|
|
348
|
+
...(perHostBudget.size > 0
|
|
349
|
+
? { perHostPromptBudget: Object.fromEntries(perHostBudget) }
|
|
350
|
+
: {}),
|
|
302
351
|
});
|
|
303
352
|
}
|
|
304
353
|
catch (error) {
|
|
305
354
|
const message = error instanceof Error ? error.message : String(error);
|
|
306
|
-
return `injection_budget_check_failed: could not recompute the prompt injection projection (${message}); the activation is committed but its injection status is unverified. nextAction=check .pd/state.db readability and re-run pd runtime activation list`;
|
|
355
|
+
return `injection_budget_check_failed: could not recompute the prompt injection projection from the runtime budget observation (${message}); the activation is committed but its injection status is unverified. nextAction=check .pd/state.db and event log readability and re-run pd runtime activation list`;
|
|
307
356
|
}
|
|
308
357
|
if (resolution.status === 'unconfirmed') {
|
|
309
|
-
|
|
358
|
+
// Each per-host forecast ran under ITS OWN budget basis (observation,
|
|
359
|
+
// runtime fallback, stale fallback, or no observation) — the headline
|
|
360
|
+
// numbers must never imply a single host's confirmation covers them all.
|
|
361
|
+
return `injection_capacity_unconfirmed: the activation is committed but this workspace's effective injection route cannot be confirmed (${resolution.decision.unconfirmedReason ?? 'unknown reason'}); per-host forecasts: ${resolution.perHost.map((entry) => `${entry.hostKind}=${entry.projection.route} (${entry.projection.usedChars}c/${entry.projection.budget}c${entry.projection.truncated ? ', truncated' : ''}) ${describeBudgetBasis(entry.hostKind, budgetFacts)}`).join('; ')}. nextAction=${resolution.decision.nextAction ?? 'pass an explicit target host (openclaw|codex|workbuddy)'}`;
|
|
310
362
|
}
|
|
311
363
|
const { projection } = resolution;
|
|
312
364
|
if (projection.injectedActivationIds.includes(activationId)) {
|
|
313
365
|
return undefined;
|
|
314
366
|
}
|
|
367
|
+
// Never let a stale observation masquerade as the current budget in the
|
|
368
|
+
// warning: name the basis the forecast actually ran under.
|
|
369
|
+
const decidedHost = resolution.decision.hostKind;
|
|
370
|
+
const budgetBasisNote = decidedHost === undefined ? '' : ` ${describeBudgetBasis(decidedHost, budgetFacts)}`;
|
|
315
371
|
const projectionDetail = projection.warnings.length > 0 ? ` projection warnings: ${projection.warnings.join(' | ')}` : '';
|
|
316
372
|
// AC-02/R-A3: oversized is independent of `truncated` — say WHY first.
|
|
317
373
|
if (projection.oversizedActivationIds.includes(activationId)) {
|
|
318
374
|
const scopeNote = projection.route === 'shared_render'
|
|
319
375
|
? 'the full directive render of this single principle exceeds the cap'
|
|
320
376
|
: 'its own list entry exceeds the cap';
|
|
321
|
-
return `injection_oversized: the activation is committed but ${scopeNote} (${projection.budget} chars, measured in UTF-16 units on the ${projection.route} route) — it can never enter the prompt as written, regardless of rotation or free capacity
|
|
377
|
+
return `injection_oversized: the activation is committed but ${scopeNote} (${projection.budget} chars, measured in UTF-16 units on the ${projection.route} route) — it can never enter the prompt as written, regardless of rotation or free capacity.${budgetBasisNote} nextAction=shorten the statement via the activations page "modify to injectable version" entry, or deactivate this activation`;
|
|
322
378
|
}
|
|
323
379
|
if (!projection.truncated) {
|
|
324
380
|
// Not included and the budget did NOT truncate — the projection skipped
|
|
@@ -333,7 +389,7 @@ export class ApprovalsConsoleModel {
|
|
|
333
389
|
// empty reachability set falls through to the starvation branch below.
|
|
334
390
|
if (projection.productionRotates && projection.eventuallyInjectedActivationIds.includes(activationId)) {
|
|
335
391
|
const window = projection.eligibleCount;
|
|
336
|
-
return `injection_budget_queued: the activation is committed but sits outside the CURRENT prompt injection window (${projection.budget}c; ${projection.injectedActivationIds.length} of ${window} eligible activations injected this round; production selection policy fair_rotation_v1)
|
|
392
|
+
return `injection_budget_queued: the activation is committed but sits outside the CURRENT prompt injection window (${projection.budget}c; ${projection.injectedActivationIds.length} of ${window} eligible activations injected this round; production selection policy fair_rotation_v1).${budgetBasisNote} Fair rotation advances the window by one position per recorded user turn, so this principle WILL enter agent behavior within at most ${window} consecutive user turns of a continuously advancing session — deactivating older principles is NOT required. nextAction=none; verify presence via the activations page or the prompt injection telemetry`;
|
|
337
393
|
}
|
|
338
394
|
// PR-1894: report the PRODUCTION policy, not the policy this forecast
|
|
339
395
|
// happened to run. The console holds no session round key, so its own
|
|
@@ -348,7 +404,7 @@ export class ApprovalsConsoleModel {
|
|
|
348
404
|
const unreachableNote = projection.productionRotates
|
|
349
405
|
? ' (this forecast ran without a round key, so the window shown is the legacy FIFO prefix; the entry is unreachable because it exceeds the budget on its own, not because of its position)'
|
|
350
406
|
: '';
|
|
351
|
-
return `injection_budget_excluded: the activation is committed but the prompt injection budget (${projection.budget}c) is full and this activation is not reachable under the ${policyNote}; ${projection.injectedActivationIds.length}/${projection.eligibleCount} eligible injected${unreachableNote}
|
|
407
|
+
return `injection_budget_excluded: the activation is committed but the prompt injection budget (${projection.budget}c) is full and this activation is not reachable under the ${policyNote}; ${projection.injectedActivationIds.length}/${projection.eligibleCount} eligible injected${unreachableNote}.${budgetBasisNote} This principle will NOT enter agent behavior until capacity frees up. nextAction=review the activations page and deactivate superseded principles`;
|
|
352
408
|
}
|
|
353
409
|
/**
|
|
354
410
|
* Bug-O L3b fix: upgrade the ledger principle linked to `artifactId` from
|
|
@@ -11,11 +11,23 @@ export interface ApprovalGroup {
|
|
|
11
11
|
/**
|
|
12
12
|
* PR-1894: whether this candidate's own serialized prompt entry fits the
|
|
13
13
|
* injection budget on its own. `false` means the entry is oversized, so fair
|
|
14
|
-
* rotation can NEVER inject it
|
|
14
|
+
* rotation can NEVER inject it — the pre-approval badge must not promise it
|
|
15
15
|
* will come around. Computed server-side with the production serializer
|
|
16
16
|
* (the client has no access to the escaped, serialized size).
|
|
17
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: `true` now means "fits within the
|
|
18
|
+
* supported budget range" — see `promptBudgetTierFit` for whether that is
|
|
19
|
+
* unconditional (fits every tier) or model-dependent.
|
|
17
20
|
*/
|
|
18
21
|
fitsPromptBudget?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §6.3: honest tier classification of
|
|
24
|
+
* the candidate's own serialized entry against the supported
|
|
25
|
+
* [min, max] budget range. `model_dependent` = fits only on
|
|
26
|
+
* larger-context models — the badge must qualify rotation promises, not
|
|
27
|
+
* promise unconditionally. `unconfirmed` mirrors the precheck's honest
|
|
28
|
+
* unknown.
|
|
29
|
+
*/
|
|
30
|
+
promptBudgetTierFit?: 'fits_all_tiers' | 'model_dependent' | 'exceeds_max_tier' | 'unconfirmed';
|
|
19
31
|
revisionReviewUnavailable?: boolean;
|
|
20
32
|
revisionReview?: {
|
|
21
33
|
oldStatement: string;
|
|
@@ -62,6 +74,18 @@ export declare class ApprovalsGroupedConsoleModel {
|
|
|
62
74
|
* only: a projection failure omits the field — the approve-time precheck
|
|
63
75
|
* (B1) remains the fail-loud gate (rc-9), so this must never fail the
|
|
64
76
|
* grouped read.
|
|
77
|
+
*
|
|
78
|
+
* PRI-1017: the forecast now runs under the workspace's most recent
|
|
79
|
+
* RUNTIME BUDGET OBSERVATION (the budget the last real injection used,
|
|
80
|
+
* read from the existing `runtime_v2_prompt_activations_injected` events)
|
|
81
|
+
* instead of the console's own parameterless fallback — SPEC
|
|
82
|
+
* §6.1 forbids the injection and the console forecasting under different
|
|
83
|
+
* budgets. The observation is a PAST fact: `budgetObservedAt` travels with
|
|
84
|
+
* the payload so the UI labels it as such. No observation, an observation
|
|
85
|
+
* for a different host, or an observation past the freshness bound ⇒ the
|
|
86
|
+
* honest fixed-fallback projection (with `runtime_budget_observation_stale`
|
|
87
|
+
* when a stale observation exists). No second budget state is created:
|
|
88
|
+
* the events remain the single runtime record and this is a read model.
|
|
65
89
|
*/
|
|
66
90
|
private readPromptInjectionBudgetStatus;
|
|
67
91
|
dispose(): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SqliteConnection, SqliteApprovalQueueStore, SqlitePIArtifactStore, ApprovalQueue, RUNTIME_V2_PRINCIPLE_BUDGET, detectPromptReplacementTarget, SqliteActivationStateStore, } from '@principles/core/runtime-v2';
|
|
2
|
-
import { checkPromptArtifactDeliverability, resolveLivePromptInjectionProjection, } from '@principles/host-runtime';
|
|
2
|
+
import { checkPromptArtifactDeliverability, resolveLivePromptInjectionProjection, RUNTIME_BUDGET_OBSERVATION_STALE_REASON, runtimeBudgetObservationToResolution, selectConsoleRuntimeBudgetFacts, } from '@principles/host-runtime';
|
|
3
3
|
import { loadLedger } from '@principles/core/principle-tree-ledger';
|
|
4
4
|
import { createArtifactPrincipleResolutionDeps, resolveArtifactPrincipleId, } from './artifact-principle-resolver.js';
|
|
5
5
|
import * as fs from 'node:fs';
|
|
@@ -104,19 +104,21 @@ function extractCandidateDescription(contentJson) {
|
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* PR-1894 → PD_PROMPT_CAPACITY_V1 R-A2
|
|
108
|
-
*
|
|
107
|
+
* PR-1894 → PD_PROMPT_CAPACITY_V1 R-A2 → SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001:
|
|
108
|
+
* classify this candidate's own prompt entry against the SUPPORTED budget
|
|
109
|
+
* range (not one fixed number — approval time cannot know the executing
|
|
110
|
+
* model).
|
|
109
111
|
*
|
|
110
|
-
* The judgment
|
|
112
|
+
* The judgment comes from the ONE shared host-runtime precheck
|
|
111
113
|
* (`checkPromptArtifactDeliverability`): the REAL resolved principle id (not a
|
|
112
114
|
* fixed 36-char UUID allowance — a title-derived id can only be longer), the
|
|
113
115
|
* effective route's own serializer (list entry vs full directive render incl.
|
|
114
116
|
* the self-report footer), and an honest `unconfirmed` verdict when the route
|
|
115
|
-
* or the artifact content cannot be confirmed. `
|
|
116
|
-
* conservative answer
|
|
117
|
+
* or the artifact content cannot be confirmed. `unconfirmed` is also the
|
|
118
|
+
* conservative answer when the precheck itself fails — an unknown size must
|
|
117
119
|
* never become a rotation promise (rc-9).
|
|
118
120
|
*/
|
|
119
|
-
async function
|
|
121
|
+
async function candidatePromptBudgetTierFit(workspaceDir, artifact, targetHost) {
|
|
120
122
|
const deliverability = await checkPromptArtifactDeliverability({
|
|
121
123
|
workspaceDir,
|
|
122
124
|
targetHost,
|
|
@@ -130,15 +132,31 @@ async function candidateFitsPromptBudget(workspaceDir, artifact, targetHost) {
|
|
|
130
132
|
: null,
|
|
131
133
|
});
|
|
132
134
|
if (deliverability.status === 'deliverable')
|
|
133
|
-
return
|
|
135
|
+
return 'fits_all_tiers';
|
|
136
|
+
if (deliverability.status === 'model_dependent')
|
|
137
|
+
return 'model_dependent';
|
|
134
138
|
if (deliverability.status === 'undeliverable')
|
|
135
|
-
return
|
|
136
|
-
//
|
|
137
|
-
//
|
|
139
|
+
return 'exceeds_max_tier';
|
|
140
|
+
// Route/artifact unconfirmed: aggregate the per-host tier verdicts
|
|
141
|
+
// MONOTONICALLY. The list route bills ~700 fewer chars than the shared
|
|
142
|
+
// route for the same entry (fixed wrapper delta), so "shared exceeds but
|
|
143
|
+
// list does not" is REACHABLE (e.g. a ~15300-char statement: list ≈15341
|
|
144
|
+
// = model_dependent, shared ≈16058 = exceeds). Collapsing that to
|
|
145
|
+
// exceeds_max_tier would tell an OpenClaw owner their entry can never
|
|
146
|
+
// inject when a large-window list-route host could carry it. Only when
|
|
147
|
+
// EVERY applicable host exceeds is "oversized everywhere" true; any
|
|
148
|
+
// disagreement leaves the honest model_dependent. Without per-host
|
|
149
|
+
// facts, do not promise at all.
|
|
138
150
|
if (deliverability.perHost && deliverability.perHost.length > 0) {
|
|
139
|
-
|
|
151
|
+
if (deliverability.perHost.every((entry) => entry.tierFit === 'exceeds_max_tier')) {
|
|
152
|
+
return 'exceeds_max_tier';
|
|
153
|
+
}
|
|
154
|
+
if (deliverability.perHost.every((entry) => entry.tierFit === 'fits_all_tiers')) {
|
|
155
|
+
return 'fits_all_tiers';
|
|
156
|
+
}
|
|
157
|
+
return 'model_dependent';
|
|
140
158
|
}
|
|
141
|
-
return
|
|
159
|
+
return 'unconfirmed';
|
|
142
160
|
}
|
|
143
161
|
export class ApprovalsGroupedConsoleModel {
|
|
144
162
|
workspaceDir;
|
|
@@ -231,14 +249,14 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
231
249
|
if (!detection.ok || (detection.target !== null && !revisionReviews.has(artifact.artifactId)))
|
|
232
250
|
revisionReviewUnavailable.add(artifact.artifactId);
|
|
233
251
|
}
|
|
234
|
-
artifactFitsBudgetMap.set(approval.artifactId, await
|
|
252
|
+
artifactFitsBudgetMap.set(approval.artifactId, await candidatePromptBudgetTierFit(this.workspaceDir, artifact, targetHost));
|
|
235
253
|
artifactUnavailableMap.set(approval.artifactId, artifact === null);
|
|
236
254
|
}
|
|
237
255
|
catch (err) {
|
|
238
256
|
if (isMissingTableError(err)) {
|
|
239
257
|
artifactPrincipleMap.set(approval.artifactId, null);
|
|
240
258
|
artifactDescriptionMap.set(approval.artifactId, null);
|
|
241
|
-
artifactFitsBudgetMap.set(approval.artifactId,
|
|
259
|
+
artifactFitsBudgetMap.set(approval.artifactId, 'unconfirmed');
|
|
242
260
|
artifactUnavailableMap.set(approval.artifactId, true);
|
|
243
261
|
}
|
|
244
262
|
else {
|
|
@@ -302,10 +320,18 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
302
320
|
principleTitle,
|
|
303
321
|
candidateDescription,
|
|
304
322
|
status,
|
|
305
|
-
// PR-1894: undefined-safe
|
|
323
|
+
// PR-1894: undefined-safe — absent when the artifact could not be
|
|
306
324
|
// read, which the UI treats as "do not promise rotation".
|
|
325
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001: the boolean stays the
|
|
326
|
+
// rotation-promise signal (true = within the supported budget
|
|
327
|
+
// range); the tier fit carries the honest fits-everywhere vs
|
|
328
|
+
// model-dependent distinction.
|
|
307
329
|
...(firstArtifactId !== undefined && artifactFitsBudgetMap.has(firstArtifactId)
|
|
308
|
-
? {
|
|
330
|
+
? {
|
|
331
|
+
fitsPromptBudget: artifactFitsBudgetMap.get(firstArtifactId) === 'fits_all_tiers'
|
|
332
|
+
|| artifactFitsBudgetMap.get(firstArtifactId) === 'model_dependent',
|
|
333
|
+
promptBudgetTierFit: artifactFitsBudgetMap.get(firstArtifactId),
|
|
334
|
+
}
|
|
309
335
|
: {}),
|
|
310
336
|
// PRI-940: absent when the pinned artifact is readable — same
|
|
311
337
|
// spread style as fitsPromptBudget so the wire contract stays
|
|
@@ -345,16 +371,48 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
345
371
|
* only: a projection failure omits the field — the approve-time precheck
|
|
346
372
|
* (B1) remains the fail-loud gate (rc-9), so this must never fail the
|
|
347
373
|
* grouped read.
|
|
374
|
+
*
|
|
375
|
+
* PRI-1017: the forecast now runs under the workspace's most recent
|
|
376
|
+
* RUNTIME BUDGET OBSERVATION (the budget the last real injection used,
|
|
377
|
+
* read from the existing `runtime_v2_prompt_activations_injected` events)
|
|
378
|
+
* instead of the console's own parameterless fallback — SPEC
|
|
379
|
+
* §6.1 forbids the injection and the console forecasting under different
|
|
380
|
+
* budgets. The observation is a PAST fact: `budgetObservedAt` travels with
|
|
381
|
+
* the payload so the UI labels it as such. No observation, an observation
|
|
382
|
+
* for a different host, or an observation past the freshness bound ⇒ the
|
|
383
|
+
* honest fixed-fallback projection (with `runtime_budget_observation_stale`
|
|
384
|
+
* when a stale observation exists). No second budget state is created:
|
|
385
|
+
* the events remain the single runtime record and this is a read model.
|
|
348
386
|
*/
|
|
349
387
|
async readPromptInjectionBudgetStatus(targetHost) {
|
|
350
388
|
try {
|
|
389
|
+
const facts = selectConsoleRuntimeBudgetFacts({
|
|
390
|
+
workspaceDir: this.workspaceDir,
|
|
391
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
392
|
+
});
|
|
393
|
+
const perHostBudget = new Map([...facts.perHost].map(([hostKind, observation]) => [hostKind, runtimeBudgetObservationToResolution(observation)]));
|
|
351
394
|
const resolution = await resolveLivePromptInjectionProjection({
|
|
352
395
|
workspaceDir: this.workspaceDir,
|
|
353
|
-
targetHost,
|
|
396
|
+
...(targetHost !== undefined ? { targetHost } : {}),
|
|
397
|
+
...(facts.selected !== undefined
|
|
398
|
+
? { promptBudget: runtimeBudgetObservationToResolution(facts.selected) }
|
|
399
|
+
: {}),
|
|
400
|
+
...(perHostBudget.size > 0
|
|
401
|
+
? { perHostPromptBudget: Object.fromEntries(perHostBudget) }
|
|
402
|
+
: {}),
|
|
354
403
|
});
|
|
355
404
|
if (resolution.status === 'unconfirmed') {
|
|
356
405
|
// The numbers still come from the flag-based view so the badge can
|
|
357
406
|
// show SOMETHING, but capacityStatus marks them not authoritative.
|
|
407
|
+
// PRI-1017: each per-host forecast runs under THAT host's own
|
|
408
|
+
// observation (or its own fallback) — no host's window is smuggled
|
|
409
|
+
// into another host's number, and the headline reference is never
|
|
410
|
+
// presented as the current host's actual budget (the banner says so).
|
|
411
|
+
// The top-level numbers are a legacy REFERENCE view only (required
|
|
412
|
+
// fields for old consumers). Under `capacityStatus: 'unconfirmed'`
|
|
413
|
+
// they bind to no host, so the UI must never present them as the
|
|
414
|
+
// current host's actual budget — every host's own basis is in
|
|
415
|
+
// `perHost`, including its own fallback reason.
|
|
358
416
|
const reference = resolution.perHost[0]?.projection;
|
|
359
417
|
return {
|
|
360
418
|
promptInjection: {
|
|
@@ -364,16 +422,37 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
364
422
|
capacityStatus: 'unconfirmed',
|
|
365
423
|
unconfirmedReason: resolution.decision.unconfirmedReason ?? 'effective injection route cannot be confirmed',
|
|
366
424
|
nextAction: resolution.decision.nextAction ?? 'pass an explicit target host (openclaw|codex|workbuddy)',
|
|
367
|
-
perHost: resolution.perHost.map((entry) =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
425
|
+
perHost: resolution.perHost.map((entry) => {
|
|
426
|
+
const observation = facts.perHost.get(entry.hostKind);
|
|
427
|
+
const stale = observation === undefined ? facts.stalePerHost.get(entry.hostKind) : undefined;
|
|
428
|
+
return {
|
|
429
|
+
hostKind: entry.hostKind,
|
|
430
|
+
route: entry.projection.route,
|
|
431
|
+
usedChars: entry.projection.usedChars,
|
|
432
|
+
truncated: entry.projection.truncated,
|
|
433
|
+
budget: entry.projection.budget,
|
|
434
|
+
...(entry.projection.budgetCapacitySource !== undefined ? { budgetCapacitySource: entry.projection.budgetCapacitySource } : {}),
|
|
435
|
+
...(entry.projection.budgetCapacityStatus !== undefined ? { budgetCapacityStatus: entry.projection.budgetCapacityStatus } : {}),
|
|
436
|
+
...(observation !== undefined ? { budgetObservedAt: observation.observedAt } : {}),
|
|
437
|
+
...(stale !== undefined
|
|
438
|
+
? { budgetFallbackReason: RUNTIME_BUDGET_OBSERVATION_STALE_REASON }
|
|
439
|
+
: entry.projection.budgetFallbackReason !== undefined
|
|
440
|
+
? { budgetFallbackReason: entry.projection.budgetFallbackReason }
|
|
441
|
+
: {}),
|
|
442
|
+
};
|
|
443
|
+
}),
|
|
373
444
|
},
|
|
374
445
|
};
|
|
375
446
|
}
|
|
376
447
|
const { projection } = resolution;
|
|
448
|
+
// Bind the observation to the host the route actually resolved to — not
|
|
449
|
+
// to a request parameter that may be absent (single-host workspace) or
|
|
450
|
+
// to whichever host happens to appear first.
|
|
451
|
+
const decidedHost = resolution.decision.hostKind;
|
|
452
|
+
const usedObservation = decidedHost !== undefined ? facts.perHost.get(decidedHost) : undefined;
|
|
453
|
+
const staleObservation = decidedHost !== undefined && usedObservation === undefined
|
|
454
|
+
? facts.stalePerHost.get(decidedHost)
|
|
455
|
+
: undefined;
|
|
377
456
|
return {
|
|
378
457
|
promptInjection: {
|
|
379
458
|
budget: projection.budget,
|
|
@@ -391,6 +470,19 @@ export class ApprovalsGroupedConsoleModel {
|
|
|
391
470
|
...(projection.fullRenderChars !== undefined ? { fullRenderChars: projection.fullRenderChars } : {}),
|
|
392
471
|
oversizedActivationIds: projection.oversizedActivationIds,
|
|
393
472
|
oversizedDiagnosticTruncated: projection.oversizedDiagnosticTruncated,
|
|
473
|
+
// SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §7: budget provenance — when
|
|
474
|
+
// a runtime budget OBSERVATION backs this forecast (PRI-1017) the
|
|
475
|
+
// numbers are the ones the last real injection used; otherwise this
|
|
476
|
+
// is the fixed-budget view and the UI labels it as such.
|
|
477
|
+
...(projection.budgetCapacitySource !== undefined ? { budgetCapacitySource: projection.budgetCapacitySource } : {}),
|
|
478
|
+
...(projection.budgetCapacityStatus !== undefined ? { budgetCapacityStatus: projection.budgetCapacityStatus } : {}),
|
|
479
|
+
...(projection.budgetContextWindowTokens != null ? { budgetContextWindowTokens: projection.budgetContextWindowTokens } : {}),
|
|
480
|
+
...(projection.budgetFallbackReason !== undefined ? { budgetFallbackReason: projection.budgetFallbackReason } : {}),
|
|
481
|
+
// A stale observation is NOT a current fact: the honest reason code
|
|
482
|
+
// replaces the generic absence code so the UI can say why the
|
|
483
|
+
// runtime's last budget was not used.
|
|
484
|
+
...(staleObservation !== undefined ? { budgetFallbackReason: RUNTIME_BUDGET_OBSERVATION_STALE_REASON } : {}),
|
|
485
|
+
...(usedObservation !== undefined ? { budgetObservedAt: usedObservation.observedAt } : {}),
|
|
394
486
|
},
|
|
395
487
|
};
|
|
396
488
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApprovalsGroupedConsoleModel } from '../models/ApprovalsGroupedConsoleModel.js';
|
|
2
2
|
import { sendSuccess, sendError, sendNotFound, sendBadRequest } from '../utils/response.js';
|
|
3
|
+
import { isPromptInjectionTargetHost } from '@principles/host-runtime';
|
|
3
4
|
const models = new Map();
|
|
4
5
|
function getModel(workspaceDir) {
|
|
5
6
|
let model = models.get(workspaceDir);
|
|
@@ -17,12 +18,14 @@ export async function handleApprovalsGroupedRoute(req, res, workspaceDir) {
|
|
|
17
18
|
// PD_PROMPT_CAPACITY_V1 R-A1: request-level target host for the capacity
|
|
18
19
|
// forecast — the request-scoped selection entry when the workspace's
|
|
19
20
|
// applicable hosts would take different routes. No new persisted config.
|
|
21
|
+
// The roster is the host-runtime one (openclaw|codex|workbuddy) — the
|
|
22
|
+
// focus page offers all three, so the API must accept all three.
|
|
20
23
|
const url = new URL(req.url ?? '/api/v1/approvals/grouped', 'http://localhost');
|
|
21
24
|
let targetHost;
|
|
22
25
|
const rawHost = url.searchParams.get('host');
|
|
23
26
|
if (rawHost !== null) {
|
|
24
|
-
if (rawHost
|
|
25
|
-
sendBadRequest(res, 'host must be one of: openclaw, codex');
|
|
27
|
+
if (!isPromptInjectionTargetHost(rawHost)) {
|
|
28
|
+
sendBadRequest(res, 'host must be one of: openclaw, codex, workbuddy');
|
|
26
29
|
return;
|
|
27
30
|
}
|
|
28
31
|
targetHost = rawHost;
|