mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
Reviewer regression: `!= null` treated an EMPTY env value ("CLAUDE_MAX_PROMPT_CHARS="
in an EnvironmentFile/.env) as explicit -> parseInt("") = NaN -> guard disabled + a
false "[System] Note: 0 older messages were truncated" injected into every prompt.
Extracted resolvePromptCharBudget() (truthiness contract, matching the old
`parseInt(env || default)` behavior) into lib/prompt.mjs so the semantics are
mutation-tested: switching back to != null fails the empty-string test. +2 tests, 349/0.
Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
55 lines
3.2 KiB
JavaScript
55 lines
3.2 KiB
JavaScript
// lib/prompt.mjs — pure operator-append step for the system prompt.
|
||
//
|
||
// Extracted so the rule is unit-testable (the suite never imports server.mjs — it
|
||
// boots a listener). server.mjs composes wrapper + client system messages exactly as
|
||
// before, then passes the result through this. With CLAUDE_SYSTEM_PROMPT unset the
|
||
// return is the INPUT STRING UNCHANGED — the default path stays byte-for-byte
|
||
// identical, which is the repo's bar for touching a request-shaping function.
|
||
//
|
||
// The operator prompt goes LAST deliberately: a server-wide directive ("answer in
|
||
// Chinese") should read as the final instruction, not something a client system
|
||
// message overrides by coming later. Whitespace-only values are treated as unset —
|
||
// a stray space in a service unit's Environment= line must not inject "\n\n " into
|
||
// every request.
|
||
export function appendOperatorPrompt(base, operatorAppend) {
|
||
const op = typeof operatorAppend === "string" ? operatorAppend.trim() : "";
|
||
return op ? `${base}\n\n${op}` : base;
|
||
}
|
||
|
||
// Derive the default prompt-char budget from the models.json SPOT (ADR 0009).
|
||
//
|
||
// The old default was a hand-set constant (150000 chars ≈ 37.5k English tokens) from the
|
||
// 200k-window era — silently far below what the advertised contextWindow promises. Instead
|
||
// of picking a new constant that will also rot, the default now FOLLOWS the SPOT:
|
||
//
|
||
// budget = max(models[].contextWindow) × charsPerToken
|
||
//
|
||
// charsPerToken = 3 is deliberately conservative: English runs ~4 chars/token, CJK ~1–1.5.
|
||
// At ×3, a 200k-token window yields 600,000 chars — full window for English, and CJK text
|
||
// hits the model's real window at roughly the same point the cap fires, so we truncate
|
||
// (graceful, tail-first) rather than let the upstream reject the request outright.
|
||
//
|
||
// The floor guards the degenerate cases (empty/missing models[], absent contextWindow):
|
||
// fall back to the historical constant rather than 0 — a zero budget would truncate every
|
||
// request to nothing, which is fail-OPEN in the "serve garbage" sense. CLAUDE_MAX_PROMPT_CHARS
|
||
// remains an absolute operator override at the call site (server.mjs); this function is only
|
||
// the unset-env default.
|
||
export function derivePromptCharBudget(models, { charsPerToken = 3, floor = 150000 } = {}) {
|
||
const windows = (Array.isArray(models) ? models : [])
|
||
.map(m => m?.contextWindow)
|
||
.filter(w => Number.isFinite(w) && w > 0);
|
||
if (windows.length === 0) return floor;
|
||
return Math.max(floor, Math.max(...windows) * charsPerToken);
|
||
}
|
||
|
||
// Resolve the effective budget from the env var + SPOT. TRUTHINESS (not != null) on the env
|
||
// value deliberately: an EMPTY value ("CLAUDE_MAX_PROMPT_CHARS=" in a systemd EnvironmentFile
|
||
// or .env) must mean "use the default" — exactly the old `parseInt(env || "150000")` contract.
|
||
// Treating "" as explicit gives parseInt("") = NaN, and a NaN cap silently DISABLES the
|
||
// runaway-context guard while injecting a false "[System] Note: 0 older messages were
|
||
// truncated" line into every prompt (caught in PR #179 review). Non-empty garbage still
|
||
// parses to NaN — the pre-existing class, slated for parseIntEnv routing in PR #154.
|
||
export function resolvePromptCharBudget(rawEnv, models, opts) {
|
||
return rawEnv ? parseInt(rawEnv, 10) : derivePromptCharBudget(models, opts);
|
||
}
|