Skip to content

PromptProcessing: session-name validator rejects inference names outside a hardcoded verb whitelist, while the deterministic fallback passes by construction #1718

Description

@xmasyx

Summary

In LifeOS/install/hooks/PromptProcessing.hook.ts, session names produced by inference are validated against isValidSessionName(), which requires the first word to be a member of a hand-maintained 96-verb whitelist (ACTION_VERBS). Common, perfectly grammatical verbs a model naturally picks — decide, refine, adjust, reposition, tune, polish, verify, commit — are not in that set, so good inference names are rejected.

When the inference name is rejected, the code falls through to the deterministic extractFallbackName(). Its last-resort Strategy 4 prepends the literal string 'Review' (line 545) precisely so its output will satisfy the same validator. review is in ACTION_VERBS.

Net effect: the validator filters out the good source and rubber-stamps the weak one. The deterministic fallback passes by construction; the model's name cannot.

Evidence (verified against this repo at HEAD)

  • hooks/PromptProcessing.hook.ts:576if (!ACTION_VERBS.has(first) || BANNED_SESSION_LEAD.has(first)) return false;
  • hooks/PromptProcessing.hook.ts:1056 — the same validator is applied to the inference-produced label.
  • hooks/PromptProcessing.hook.ts:545const candidate = ['Review', ...picks].slice(0, 5).join(' ');
  • decide, refine, adjust, reposition are all absent from the ACTION_VERBS set (lines 365–380).

Reproduction

  1. Submit a first prompt whose natural 5-word summary leads with a verb outside ACTION_VERBS — e.g. anything a model would summarize as Decide Icon Placement In Statusline or Refine Dashboard Layout Spacing.
  2. Observe in MEMORY/OBSERVABILITY/prompt-processing.jsonl that inference returned that name ("source":"inference", session_name populated).
  3. Observe that MEMORY/STATE/session-names.json instead holds a Review … label, i.e. the Strategy-4 fallback, and that the statusline renders it.

Because isFirstPrompt is keyed on "no name stored yet", a rejection also causes the naming attempt to repeat on the next prompt, so the final label can be derived from a follow-up message rather than the one that states the task.

Root cause

isValidSessionName() was designed as a grammar guard for extractFallbackName(), which assembles names out of words scraped from the prompt and genuinely needs a lexical whitelist to avoid word salad. Applying the same guard to a language model — which is being asked in the prompt for "a grammatical task phrase" — inverts its purpose: it caps the good generator at the vocabulary of a hardcoded list that will always lag real usage.

Secondary defect (same file, independent)

The naming word lists (NOISE_WORDS, ACTION_VERBS, QUESTION_WORDS, META_VERBS) are English-only, and both prompt cleaners use .replace(/[^a-zA-Z\s]/g, ' ') (lines 398 and 442), which deletes accented Latin letters along with punctuation.

Consequences for any user writing in a language other than English:

  • No word is ever recognized as noise, so function words become "content words" and land in the title.
  • No word is ever recognized as an action verb, so Strategies 1–3 never validate and every name falls to Strategy 4, i.e. Review + four scraped words.
  • Accented words are silently truncated before being compared to the lists (a word ending in an accented character loses it, so the truncated stem never matches anything).

This is much less visible once the primary defect is fixed — the deterministic path becomes a genuine fallback rather than the usual outcome — but it still produces unreadable names whenever inference is unavailable.

Proposed fix

1. Validate the inference name for shape, not vocabulary. Keep the guards that carry real information (word count, minimum word length, no fragment punctuation, no meta-instruction lead) and drop the whitelist requirement. Add alongside isValidSessionName():

/**
 * Validation for INFERENCE-produced names. `isValidSessionName` exists to
 * discipline `extractFallbackName`, which scrapes words from the prompt and
 * needs a lexical whitelist to avoid word salad. Applying it to the model
 * inverted its purpose — it rejected valid names ("Decide …", "Refine …")
 * while Strategy 4 passed by construction, since it prepends 'Review'.
 */
function isValidInferredSessionName(name: string): boolean {
  const words = name.split(/\s+/).filter(w => w.length > 0);
  if (words.length < 2 || words.length > 5) return false;
  if (words.some(w => w.length < 2)) return false;
  if (/[,;:/\\]/.test(name)) return false;
  if (BANNED_SESSION_LEAD.has(words[0].toLowerCase())) return false;
  const meaningful = words.filter(w =>
    /^[A-Z]{2,8}$/.test(w) || !NOISE_WORDS.has(w.toLowerCase()));
  return meaningful.length >= 2;
}

and at line 1056:

-            if (label && nameWords.length >= 2 && nameWords.every(w => w.length >= 2) && !hasProfanity && isValidSessionName(label)) {
+            if (label && !hasProfanity && isValidInferredSessionName(label)) {

isValidSessionName() stays exactly as it is and keeps guarding extractFallbackName(), where the whitelist is still the right tool.

2. Stop deleting accented letters. Replace both occurrences of the cleaner character class with one that keeps Latin-1 letters (excluding × U+00D7 and ÷ U+00F7, which sit inside the block but are signs):

const NON_WORD_CHARS = /[^A-Za-zÀ-ÖØ-öø-ÿ\s]/g;

3. Optionally, make the word lists extensible per install. Even with fix 1, a non-English user's fallback path is unusable. The lists could be seeded from an optional user-supplied file (e.g. LIFEOS/USER/CONFIG/naming-words.json merged over the built-in sets) so locales can be added without patching a system file that upgrades overwrite. Adding a handful of missing English verbs (decide, refine, adjust, reposition, tune, polish, verify, commit, align, trim) is worth doing regardless.

Note on testability

main() is called unconditionally at module scope, so none of these pure functions can be imported by a test without executing the hook. Guarding it makes the naming logic testable at zero behavioural cost, since the hook is registered in settings.json as a command:

if (import.meta.main) main();
export { extractFallbackName, isValidSessionName, isValidInferredSessionName };

I have applied fixes 1 and 2 locally and covered them with two-pole tests (each asserts both what must be rejected and what must be accepted), including a probe that runs the pre-fix code from git history to confirm the tests would have failed before the change. Happy to open a PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions