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:576 — if (!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:545 — const candidate = ['Review', ...picks].slice(0, 5).join(' ');
decide, refine, adjust, reposition are all absent from the ACTION_VERBS set (lines 365–380).
Reproduction
- 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.
- Observe in
MEMORY/OBSERVABILITY/prompt-processing.jsonl that inference returned that name ("source":"inference", session_name populated).
- 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.
Summary
In
LifeOS/install/hooks/PromptProcessing.hook.ts, session names produced by inference are validated againstisValidSessionName(), 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.reviewis inACTION_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:576—if (!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:545—const candidate = ['Review', ...picks].slice(0, 5).join(' ');decide,refine,adjust,repositionare all absent from theACTION_VERBSset (lines 365–380).Reproduction
ACTION_VERBS— e.g. anything a model would summarize asDecide Icon Placement In StatuslineorRefine Dashboard Layout Spacing.MEMORY/OBSERVABILITY/prompt-processing.jsonlthat inference returned that name ("source":"inference",session_namepopulated).MEMORY/STATE/session-names.jsoninstead holds aReview …label, i.e. the Strategy-4 fallback, and that the statusline renders it.Because
isFirstPromptis 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 forextractFallbackName(), 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:
Review+ four scraped words.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():and at line 1056:
isValidSessionName()stays exactly as it is and keeps guardingextractFallbackName(), 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):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.jsonmerged 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 insettings.jsonas a command: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.