v1.13.8: system-prompt prefix stability verify-and-measure

Recon during planning disproved the original v1.13.7 (DB-cache) premise:
buildSystemPrompt already runs over inputs mtime-cached at the file layer
(BOOCHAT.md in system-prompt.ts:25, AGENTS.md global+per-project in
agents.ts:245), and DB scalars are byte-stable until edited. The output
is microsecond pure-string concat with no I/O. Skills aren't in the
prefix; tools live in a separate request body field alpha-sorted by
v1.13.3.

This batch closes the verification gap with instrumentation, not
implementation:

- system-prompt.ts: buildSystemPromptWithFingerprint canonical impl
  computes SHA-256 over the assembled prefix, runs a per-session
  Map<sessionId, lastHash> observer, emits PrefixFingerprint per call
  and PrefixDrift (with field-level changed_inputs) on hash change.
  buildSystemPrompt is now a thin shim returning .prompt.
- agents.ts: getAgentsMtimes accessor — cache-read only, no I/O.
- payload.ts: buildMessagesPayload takes optional log argument; when
  passed, emits prefix-fingerprint (info) + prefix-drift (warn).
- turn.ts + sentinel-summaries.ts: pass ctx.log at 3 production call
  sites; sentinel summaries log too so any drift across cap-hit /
  doom-loop paths surfaces.
- system-prompt.test.ts: 4 new tests (byte-identical, no-drift-on-
  stable, drift-fires-with-changed-inputs, cross-session-no-drift).

194/194 tests pass (was 190).

Smoke: 5 messages in a fresh session produced 7 prefix-fingerprint
logs (extras from buildMessagesPayload being called from sentinel
summary paths), all with identical prefix_hash and prefix_length=2907,
zero prefix-drift. Prefix is byte-stable in steady-state.

Decision: original system_prompt_cache DB table from the roadmap is
permanently dropped. The v1.12.0 mtime caches at the input layer plus
alpha tool ordering at the request body (v1.13.3) already address the
load-bearing cache-stability surfaces. Instrumentation stays so the
claim can be re-verified at any time.
This commit is contained in:
2026-05-22 13:42:18 +00:00
parent 0ce6115976
commit a0c8d212cb
6 changed files with 262 additions and 10 deletions

View File

@@ -1,3 +1,4 @@
import type { FastifyBaseLogger } from 'fastify';
import type { Sql } from '../../db.js';
import type {
Agent,
@@ -6,7 +7,7 @@ import type {
Session,
} from '../../types/api.js';
import * as compaction from '../compaction.js';
import { buildSystemPrompt } from '../system-prompt.js';
import { buildSystemPromptWithFingerprint } from '../system-prompt.js';
import { isAnySentinel } from './sentinels.js';
import { PRUNE_TRIGGER_TOKENS, prune } from './prune.js';
import type { InferenceContext } from './turn.js';
@@ -31,14 +32,25 @@ export interface OpenAiMessage {
// v1.12: buildSystemPrompt lives in services/system-prompt.ts. It awaits the
// container-guidance loader, so this function is async too and every call
// site in inference.ts awaits the result.
// v1.13.8: optional log argument. When provided, emit prefix-fingerprint
// per call + prefix-drift when the same session sees a hash change. Tests
// omit it and exercise the byte-stability surface directly through
// buildSystemPromptWithFingerprint. The observer Map in system-prompt.ts
// updates regardless of whether log is passed.
export async function buildMessagesPayload(
session: Session,
project: Project,
history: Message[],
agent: Agent | null = null
agent: Agent | null = null,
log?: FastifyBaseLogger,
): Promise<OpenAiMessage[]> {
const out: OpenAiMessage[] = [];
const systemPrompt = await buildSystemPrompt(project, session, agent);
const { prompt: systemPrompt, fingerprint, drift } =
await buildSystemPromptWithFingerprint(project, session, agent);
if (log) {
log.info(fingerprint);
if (drift) log.warn(drift);
}
out.push({ role: 'system', content: systemPrompt });
// Find the latest compact marker — only send messages from that point onwards