Hotfix: recognize past-tense vomiting in deterministic chat safety #51

Merged
timmy merged 1 commits from timmy/48-vomiting-phrase-hotfix into main 2026-08-20 17:17:34 +00:00
7 changed files with 35 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 311 KiB

View File

@ -122,7 +122,7 @@ await page.locator('#chat-message').fill('What pattern do you see?');
await tap('#send-chat', 500); await tap('#send-chat', 500);
await page.getByText(/mostly Type 4/i).waitFor(); await page.getByText(/mostly Type 4/i).waitFor();
await caption('Hermes keeps credentials, tools, and session continuity server-side. Photos stay out of chat.', 1600); await caption('Hermes keeps credentials, tools, and session continuity server-side. Photos stay out of chat.', 1600);
await page.locator('#chat-message').fill('I have rectal bleeding'); await page.locator('#chat-message').fill('I barfed');
await tap('#send-chat', 450); await tap('#send-chat', 450);
await page.getByText(/Pause and get medical help/i).waitFor(); await page.getByText(/Pause and get medical help/i).waitFor();
if (hermesCalls !== 1) throw new Error('Urgent chat must be intercepted before Hermes'); if (hermesCalls !== 1) throw new Error('Urgent chat must be intercepted before Hermes');

View File

@ -4,7 +4,7 @@ const URGENT_TEXT_PATTERNS = Object.freeze([
['blood', /\b(?:rectal bleeding|bleeding from (?:the )?(?:rectum|bottom)|blood(?:y)? (?:in|on|with) (?:my |the )?(?:stool|poop|bowel movement)|(?:stool|poop) (?:has|contains|with) blood)\b/i], ['blood', /\b(?:rectal bleeding|bleeding from (?:the )?(?:rectum|bottom)|blood(?:y)? (?:in|on|with) (?:my |the )?(?:stool|poop|bowel movement)|(?:stool|poop) (?:has|contains|with) blood)\b/i],
['blackOrDarkRed', /\b(?:(?:black|dark[- ]?red) (?:stool|poop|bowel movement)|(?:stool|poop|bowel movement) (?:is|looks?) (?:black|dark[- ]?red))s?\b/i], ['blackOrDarkRed', /\b(?:(?:black|dark[- ]?red) (?:stool|poop|bowel movement)|(?:stool|poop|bowel movement) (?:is|looks?) (?:black|dark[- ]?red))s?\b/i],
['severePain', /\b(?:severe|constant|unrelenting) (?:abdominal|stomach|belly) pain\b/i], ['severePain', /\b(?:severe|constant|unrelenting) (?:abdominal|stomach|belly) pain\b/i],
['vomiting', /\b(?:vomit(?:ing|ed)?|throwing up)\b/i], ['vomiting', /\b(?:vomit(?:ing|ed|s)?|throw(?:ing|s)? up|threw up|thrown up|puk(?:e|ed|ing|es)|barf(?:ed|ing|s)?|upchuck(?:ed|ing|s)?|toss(?:ed|ing|es)? (?:my|your|his|her|our|their|the) cookies|los(?:e|t|ing|es) (?:my|your|his|her|our|their|the) lunch|(?:i|we|you|he|she|they|someone) (?:(?:have|had|just|already|recently|am|are|was|were|kept) )?(?:hurl(?:s|ed|ing)?|spew(?:s|ed|ing)?)(?=\s*(?:[.!?]|$|again\b|twice\b|all night\b))|emesis)\b/i],
['fever', /\bfever(?:ish)?\b/i], ['fever', /\bfever(?:ish)?\b/i],
['cannotPassGas', /\b(?:cannot|can[']?t|cant|unable to|not able to) pass gas\b/i], ['cannotPassGas', /\b(?:cannot|can[']?t|cant|unable to|not able to) pass gas\b/i],
]); ]);

View File

@ -50,9 +50,35 @@ test('detects common urgent symptom language without matching unrelated blood wo
'My stool is black', 'My stool is black',
'I have severe stomach pain', 'I have severe stomach pain',
'I am throwing up and have a fever', 'I am throwing up and have a fever',
'I threw up',
'I puked twice',
'I am barfing',
'I barfed',
'I hurled',
'She hurls',
'I am upchucking',
'I spewed',
'She spews',
'I tossed my cookies',
'She tosses her cookies',
'He tossed his cookies',
'Someone is tossing their cookies',
'I lost my lunch',
'She loses her lunch',
'He lost his lunch',
'Someone is losing their lunch',
'I have emesis',
'I am unable to pass gas', 'I am unable to pass gas',
]) assert.equal(detectUrgentText(message).urgent, true, message); ]) assert.equal(detectUrgentText(message).urgent, true, message);
assert.equal(detectUrgentText('My blood pressure was checked').urgent, false); assert.equal(detectUrgentText('My blood pressure was checked').urgent, false);
for (const nonVomiting of [
'She hurled the javelin across the field.',
'He hurls insults when angry.',
'They are hurling rocks at the wall.',
'He spewed hateful rhetoric.',
'The volcano spews ash.',
'The pipe is spewing water.',
]) assert.equal(detectUrgentText(nonVomiting).urgent, false, nonVomiting);
}); });
test('detects urgent flags or language in confirmed ledger context', () => { test('detects urgent flags or language in confirmed ledger context', () => {

View File

@ -128,6 +128,10 @@ test('authoritative chat service intercepts urgent message and ledger symptoms b
await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
const messageResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I have rectal bleeding', ledger: [] } }); const messageResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I have rectal bleeding', ledger: [] } });
const pastTenseVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I threw up', ledger: [] } });
const slangVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I barfed', ledger: [] } });
const possessiveIdiomResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'She tosses her cookies', ledger: [] } });
const thirdPersonVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'She hurls', ledger: [] } });
const ledgerResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { const ledgerResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: {
message: 'What does my journal show?', message: 'What does my journal show?',
ledger: [{ bristolType: 4, color: 'brown', note: '', symptoms: { cannotPassGas: true } }], ledger: [{ bristolType: 4, color: 'brown', note: '', symptoms: { cannotPassGas: true } }],
@ -138,7 +142,7 @@ test('authoritative chat service intercepts urgent message and ledger symptoms b
} }); } });
assert.equal(calls.length, 0); assert.equal(calls.length, 0);
for (const result of [messageResult, ledgerResult, noteResult]) { for (const result of [messageResult, pastTenseVomitingResult, slangVomitingResult, possessiveIdiomResult, thirdPersonVomitingResult, ledgerResult, noteResult]) {
assert.equal(result.safetyOverride, true); assert.equal(result.safetyOverride, true);
assert.match(result.reply, /medical help/i); assert.match(result.reply, /medical help/i);
assert.doesNotMatch(result.reply, /unsafe upstream/i); assert.doesNotMatch(result.reply, /unsafe upstream/i);

View File

@ -14,7 +14,7 @@ test('release demo visibly explains the CI-protected browser path without overst
assert.match(demo, /AI may suggest visible form, broad color, and image quality — never symptoms or diagnosis/); assert.match(demo, /AI may suggest visible form, broad color, and image quality — never symptoms or diagnosis/);
assert.match(demo, /Hermes Agent connected/); assert.match(demo, /Hermes Agent connected/);
assert.match(demo, /Photos stay out of chat/); assert.match(demo, /Photos stay out of chat/);
assert.match(demo, /I have rectal bleeding/); assert.match(demo, /I barfed/);
assert.match(demo, /Urgent language is intercepted deterministically before Hermes/); assert.match(demo, /Urgent language is intercepted deterministically before Hermes/);
assert.match(demo, /SLEEK\. SIMPLE\.<br>HERMES-POWERED\./); assert.match(demo, /SLEEK\. SIMPLE\.<br>HERMES-POWERED\./);
}); });

View File

@ -38,7 +38,7 @@ assert.equal(chatRequest.message, 'What pattern do you see?');
assert.ok(Array.isArray(chatRequest.ledger)); assert.ok(Array.isArray(chatRequest.ledger));
assert.equal(JSON.stringify(chatRequest).includes('photoDataUrl'), false, 'photos never enter chat context'); assert.equal(JSON.stringify(chatRequest).includes('photoDataUrl'), false, 'photos never enter chat context');
const previousRequest = chatRequest; const previousRequest = chatRequest;
await page.locator('#chat-message').fill('I am unable to pass gas'); await page.locator('#chat-message').fill('I barfed');
await page.locator('#send-chat').click(); await page.locator('#send-chat').click();
await page.waitForSelector('.bubble.timmy >> text=Pause and get medical help'); await page.waitForSelector('.bubble.timmy >> text=Pause and get medical help');
assert.equal(chatRequest, previousRequest, 'urgent language must be intercepted before the Hermes request'); assert.equal(chatRequest, previousRequest, 'urgent language must be intercepted before the Hermes request');