Repository navigation
Expand file tree
/
Copy pathutils.js
More file actions
700 lines (632 loc) · 29 KB
/
Copy pathutils.js
File metadata and controls
700 lines (632 loc) · 29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
const answerTrace = require('./answerTrace');
const { stripStaffLies, stripInventedLookup } = require('./honesty');
const { createHash } = require('node:crypto');
const { recordStage } = require('./answerStageEvidence');
const { isPreviousPost } = require('./supportContextFormat');
const { classifyReviewRequirement } = require('./reviewRequirement');
const { SOURCE_LABEL_NAME, SOURCE_LABEL_SEPARATOR, hasTroubleshootingStep, hasSafetyWarning } = require('./supportSteps');
const SOURCE_HEADING = new RegExp(`^\\s*${SOURCE_LABEL_NAME}\\s*${SOURCE_LABEL_SEPARATOR}`, 'iu');
const ESCALATION_PATTERNS = [
/\bbilling\b/i,
/\brefunds?\b/i,
/\bprivacy\b/i,
/\bshipping\b/i,
/\bcharged\b/i,
/\binvoice\b/i,
/\bpayment\b/i,
/cancel subscription/i,
/delete my data/i,
/\bgdpr\b/i,
];
// Things Vector cannot see or change. Do not bluff — hand off.
const ACCESS_GAP_PATTERNS = [
/\border\s*(#|number|id|status|num)\b/i,
/\b(my|the)\s+order\b/i,
/\btracking\b/i,
/\bfirmware\b/i,
/\brma\b/i,
/\bwarehouse\b/i,
/\bcustoms\b/i,
/\bproduction logs?\b/i,
/\bserver logs?\b/i,
/\bserial( number)?\b/i,
/\breplacement (device|unit|omi)\b/i,
/\bdelete my (account|data)\b/i,
];
const CONFIDENCE_THRESHOLD = 0.72;
const COOLDOWN_MS = 60_000;
// thread_id -> timestamp of last reply
const lastReplyMap = new Map();
function isOnCooldown(threadId) {
const last = lastReplyMap.get(threadId);
if (!last) return false;
return Date.now() - last < COOLDOWN_MS;
}
function markReplied(threadId) {
lastReplyMap.set(threadId, Date.now());
}
const claimedMessages = new Map();
const CLAIM_MS = 10 * 60_000;
const answeredText = new Map();
const ANSWER_REPEAT_MS = 10 * 60_000;
function answeredTextKey(channelId, authorId, text) {
const normalized = String(text || '').normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
if (!channelId || !authorId || !normalized) return '';
const hash = createHash('sha256').update(normalized).digest('hex');
return `${channelId}:${authorId}:${hash}`;
}
function wasRecentlyAnsweredText(channelId, authorId, text, now = Date.now()) {
const key = answeredTextKey(channelId, authorId, text);
const last = key && answeredText.get(key);
return Boolean(last && now - last < ANSWER_REPEAT_MS);
}
function markAnsweredText(channelId, authorId, text, now = Date.now()) {
const key = answeredTextKey(channelId, authorId, text);
if (key) answeredText.set(key, now);
}
function claimMessage(id, now = Date.now()) {
const key = String(id || '').trim();
if (!key) return true;
const prev = claimedMessages.get(key);
if (prev && now - prev < CLAIM_MS) return false;
claimedMessages.set(key, now);
return true;
}
const answeringAskers = new Map();
const ANSWER_CLAIM_MS = 2 * 60_000;
function claimAsker(key, now = Date.now()) {
const prev = answeringAskers.get(key);
if (prev && now - prev < ANSWER_CLAIM_MS) return false;
answeringAskers.set(key, now);
return true;
}
function releaseAsker(key) {
answeringAskers.delete(key);
}
function pruneTracked(now = Date.now()) {
const replyCutoff = now - 5 * 60_000;
const claimCutoff = now - CLAIM_MS;
for (const [key, ts] of lastReplyMap) {
if (ts < replyCutoff) lastReplyMap.delete(key);
}
for (const [key, ts] of claimedMessages) {
if (ts < claimCutoff) claimedMessages.delete(key);
}
for (const [key, ts] of answeredText) {
if (ts < now - ANSWER_REPEAT_MS) answeredText.delete(key);
}
}
// Prevent unbounded growth. Claims last 10 minutes; reply cooldowns last 5.
setInterval(() => {
pruneTracked();
}, 2 * 60_000).unref();
function containsEscalationKeyword(text) {
const s = String(text || '');
return ESCALATION_PATTERNS.some((re) => re.test(s));
}
function needsHumanAccess(text) {
const s = String(text || '');
return ACCESS_GAP_PATTERNS.some((re) => re.test(s));
}
function shouldEscalate(aiResponse, userMessage) {
if (aiResponse.confidence < CONFIDENCE_THRESHOLD) return true;
if (aiResponse.escalate === true) return true;
if (containsEscalationKeyword(userMessage)) return true;
if (needsHumanAccess(userMessage)) return true;
return false;
}
function typingDelay() {
const ms = 400 + Math.random() * 500;
return new Promise((resolve) => setTimeout(resolve, ms));
}
function sanitizeReply(text) {
const cleaned = String(text || '')
.replace(/\bas an ai\b(?!\s+(?:message|response|reply))\s*,?\s*/gi, '')
.split('\n')
.map((line) => line.replace(/[^\S\n]{2,}/g, ' ').trimEnd())
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
return stripInventedLookup(stripStaffLies(cleaned));
}
// Anaphoric subjects cannot stand in for a support route we just removed.
// This is reference cleanup, not a general semantic-independence parser.
const REDIRECT_REFERENCE = /^(?:[-*•]\s*|\d+[.)]\s*)?["'“‘]?(?:(?:they|them|it|that|this|those|these|there|which|ils|elles|cela|ça|ce|ces|ceci|eux|das|dies(?:e[rmns]?)?|sie|dort|diese|ellos|ellas|eso|esto|esos|estos|all[ií]|eles|elas|isso|isto|esses|estes|loro|quest[oaie]|quell[oaie]|onlar|bu|şu|o|mereka|itu|ini|они|это|там|которые)(?=[\s,;:'’]|$)|それ|これ|あれ|そこ|他们|她们|它们|这|那|그것|이것|그들)/iu;
const REDIRECT_LANGUAGES = new Set(['en','fr','de','es','pt','it','tr','id','ja','zh','ko','ru']);
function startsRedirectReference(text) {
// Match the displayed subject, not Markdown's emphasis/code/quote markers.
return REDIRECT_REFERENCE.test(String(text).replace(/[*_~`]/g, '').replace(/^\s*>\s*/, '').trim());
}
function hasIndependentDirective(text) {
const shown = String(text).replace(/[*_~`]/g, '');
return hasSafetyWarning(shown) || hasTroubleshootingStep(shown) || /https?:\/\//iu.test(shown);
}
// This bounded English grammar recognizes a current instruction, not every
// possible way to request help. Unknown wording/address mentions grant no
// routing authority. The same removed clause must establish the action.
function isCurrentContactInstruction(clause, sentence, { quoted = false } = {}) {
if (quoted) return false;
const directive = String(clause).replace(/[*_~]/g, '').replace(/^\s*(?:[-+•]|\d+[.)])\s+/, '').trim();
const at = String(sentence).indexOf(clause);
const prefix = String(sentence).slice(0, at).replace(/[*_~]/g, '');
const suffix = String(sentence).slice(at + clause.length).replace(/[*_~]/g, '');
// A completed fact can contain a past temporal phrase without conditioning
// a later independent instruction. A leading condition fragment or trailing
// condition still scopes that instruction conservatively.
if (prefix.split(/[;;]/u).some(part => /^\s*(?:if|unless|when|until|before|after|once|provided|assuming)\b|\b(?:only\s+(?:if|when|after|once)|first|otherwise|alternatively|or)\b/i.test(part)) || suffix.replace(/[;;.!?\s]/gu, '')) return false;
// A complete positive grammar binds the action to Omi support. Arbitrary
// purposes/suffixes can contain conditions or alternative recipients; they
// stay unrecognized rather than becoming a guessed current requirement.
return /^(?:for (?:confirmation|help|assistance|support|review),\s*)?(?:please\s+)?(?:you\s+(?:need to|have to|must|should)\s+)?(?:e-?mail|contact|reach out to|write to|message|get in touch with)\s+(?:(?:the|our)\s+)?(?:omi\s+support|support(?:\s+team)?|help@omi\.me|team@basedhardware\.com)(?:\s+(?:now|directly))?(?:\s+for\s+(?:confirmation|help|assistance|support|review|a review))?[.!]*$/i.test(directive);
}
function contactQuoteSpans(line, previous) {
const characters = [...line], positions = [];
let offset = 0;
for (const character of characters) { positions.push(offset); offset += character.length; }
const spans = [];
let open = previous, start = open ? 0 : -1;
const word = character => /[\p{L}\p{N}]/u.test(character || '');
for (let index = 0; index < characters.length; index++) {
const character = characters[index];
if (character === '`') {
let end = index + 1;
while (characters[end] === '`') end++;
const count = end - index;
if (!open) { open = { closing: '`', count }; start = positions[index]; }
else if (open.closing === '`' && open.count === count) {
spans.push([start, positions[end] ?? line.length]); open = null;
}
index = end - 1;
} else if (open) {
const contraction = (character === "'" || character === '’') && word(characters[index - 1]) && word(characters[index + 1]);
if (character === open.closing && !contraction) {
spans.push([start, positions[index] + character.length]); open = null;
}
} else if (["'", '‘', '"', '“'].includes(character) &&
(character !== "'" || !word(characters[index - 1]))) {
open = { closing: { "'": "'", '‘': '’', '"': '"', '“': '”' }[character] };
start = positions[index];
}
}
if (open) spans.push([start, line.length]);
return { spans, open };
}
function stripSupportRedirectWithAudit(text, {language = 'en'} = {}) {
const isRedirect = (sentence) => {
// A warning not to send secrets is not an instruction to leave this thread.
if (/\b(?:never|do not|don['’]t)\s+(?:e-?mail|send|share)|\bno\s+(?:env[ií]es|compartas)|不要(?:发送|分享)/i.test(sentence)) return false;
// Support addresses identify the alternate route without guessing its language.
if (/(?:help@omi\.me|team@basedhardware\.com)/i.test(sentence)) return true;
const supportTarget = '(?:omi\\s+support|support(?:\\s+team)?|(?:the|our|your)\\s+team|(?:a|an|the|real|live)\\s+(?:person|human|agent)|help@omi\\.me|discord\\.omi\\.me|#help\\b|(?:the\\s+)?help\\s+channel|(?:the\\s+)?contact\\s+(?:form|option|page)|(?:the\\s+)?support\\s+(?:form|page|ticket))';
const directContact = new RegExp(`\\b(?:email|e-mail|contact|reach(?:\\s+out\\s+to)?|write\\s+to|message|drop\\s+a\\s+line\\s+to|get\\s+in\\s+touch\\s+with)\\s+(?:(?:the|a|an|omi|our|your)\\s+)?${supportTarget}`, 'i');
const supportForm = /\b(?:submit\s+(?:a\s+|the\s+)?(?:support\s+)?(?:request|ticket|contact\s+form)|open\s+(?:a\s+|the\s+)?(?:support\s+)?ticket)\b/i;
const supportChannel = /\b(?:join|post\s+(?:in|on|to)|ask\s+(?:in|on))\s+(?:(?:the|a)\s+)?(?:discord\.omi\.me|#help\b|help\s+channel\b)/i;
const contactControl = /\b(?:use|select|choose|click|tap)\s+(?:the\s+)?(?:contact\s+(?:omi\s+)?support|contact\s+(?:form|option|page)|support\s+(?:form|page|ticket))\b/i;
return directContact.test(sentence) || supportForm.test(sentence) || supportChannel.test(sentence) || contactControl.test(sentence);
};
let removedReference = false;
let currentContactRequested = false;
let fenced = null;
let openQuote = null;
let continuationScope = false;
const supported = REDIRECT_LANGUAGES.has(String(language).toLowerCase().split('-')[0]);
const cleaned = String(text || '')
.split('\n')
.map((line) => {
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u);
const fence = fenceMatch?.[1];
const codeOrQuote = Boolean(fenced || fence || /^\s*>|^ {4}|^\t/.test(line));
if (fence) {
if (!fenced) fenced = { character: fence[0], length: fence.length };
else if (fence[0] === fenced.character && fence.length >= fenced.length && !fenceMatch[2].trim()) fenced = null;
}
if (!line.trim()) { removedReference = false; continuationScope = false; return line; }
if (SOURCE_HEADING.test(line)) return line;
const quote = codeOrQuote ? { spans: [[0, line.length]], open: openQuote } : contactQuoteSpans(line, openQuote);
openQuote = quote.open;
const displayed = line.replace(/^\s*(?:[-+*•]|\d+[.)])\s+/, '').replace(/[*_~]/g, '').trim();
const condition = /^(?:if|unless|when|until|before|after|once|provided|assuming)\b/i.test(displayed);
const scopeHeld = continuationScope || condition;
// An unfinished heading/condition can scope the following physical lines
// or list. Do not reinterpret that continuation as a fresh imperative.
if (!codeOrQuote && (condition || !/[.!?。!?]["'”’`]*$/u.test(displayed))) continuationScope = true;
const privateOrderIntake = sentence => /^\s*include (?:the )?order (?:number|#)/i.test(sentence);
const originalClauses = line.split(/(?<=[.!?])\s+|[;;]\s*|(?<=[。!?])\s*|\s+(?:and|y)\s+(?=(?:do\s+not|don't|never|no)\b)/iu);
if (!removedReference && !originalClauses.some(s => isRedirect(s) || privateOrderIntake(s))) return line;
let sentenceCursor = 0;
return line.split(/(?<=[.!?])\s+|(?<=[。!?])\s*/u).filter(Boolean).flatMap(sentence => {
const sentenceAt = line.indexOf(sentence, sentenceCursor);
sentenceCursor = sentenceAt + sentence.length;
const clauses = sentence.split(/[;;]\s*|\s+(?:and|y)\s+(?=(?:do\s+not|don't|never|no)\b)/iu).map(s=>s.trim()).filter(Boolean);
const removed = clauses.some(s => isRedirect(s) || privateOrderIntake(s));
if (!removed) {
if (removedReference && (!supported || startsRedirectReference(sentence)) && !hasIndependentDirective(sentence)) return [];
removedReference = false;
return [sentence];
}
removedReference = true;
let clauseCursor = 0;
return clauses.flatMap(clause => {
const clauseAt = sentence.indexOf(clause, clauseCursor);
clauseCursor = clauseAt + clause.length;
const start = sentenceAt + clauseAt, end = start + clause.length;
const quoted = scopeHeld || quote.spans.some(([left, right]) => start < right && end > left);
if (SOURCE_HEADING.test(clause)) return [clause];
if (privateOrderIntake(clause)) return [];
if (!isRedirect(clause)) {
// An unknown language is not a license to guess that a split
// clause stands alone. Protective warnings are never discarded.
if (!supported && !hasIndependentDirective(clause)) return [];
return startsRedirectReference(clause) && !hasIndependentDirective(clause) ? [] : [clause];
}
currentContactRequested ||= isCurrentContactInstruction(clause, sentence, { quoted });
const details = clause.match(/\b(?:please\s+)?include\b.+$/i);
return details && !isRedirect(details[0]) ? [`Please ${details[0].replace(/^please\s+/i, '')}`] : [];
});
}).join(' ').trim();
})
.filter(Boolean)
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
return { text: cleaned, currentContactRequested };
}
function stripSupportRedirect(text, options) {
const result=stripSupportRedirectWithAudit(text, options).text;
answerTrace.filter('support_redirect',text,result);
return result;
}
const IN_THREAD_NEXT = {
en: 'If you still need help, reply here in this thread.',
es: 'Si aún necesitas ayuda, responde aquí en este hilo.',
zh: '如果还需要帮助,请在这个讨论串中回复。',
pt: 'Se ainda precisar de ajuda, responda aqui nesta conversa.',
de: 'Wenn du noch Hilfe brauchst, antworte hier in diesem Thread.',
fr: 'Si vous avez encore besoin d’aide, répondez ici dans ce fil.',
it: 'Se hai ancora bisogno di aiuto, rispondi qui in questa conversazione.',
tr: 'Hâlâ yardıma ihtiyacınız varsa bu başlıkta yanıt verin.',
id: 'Jika masih perlu bantuan, balas di utas ini.',
ja: 'まだお困りの場合は、このスレッドに返信してください。',
'hi-Deva': 'अगर अभी भी मदद चाहिए, तो इसी थ्रेड में जवाब दें।',
'hi-Latn': 'Agar abhi bhi madad chahiye, to isi thread mein reply karein.',
};
function inThreadReply(answer, { language = 'en' } = {}) {
const original = String(answer || '').trim();
const cleaned = stripSupportRedirect(original, {language});
const normalized = text => text.replace(/\s+/g, ' ').trim();
if (normalized(cleaned) === normalized(original)) return { text: original, empty: false };
const isSource = line => SOURCE_HEADING.test(line);
const lines = cleaned.split('\n');
const sources = lines.filter(isSource);
const body = lines.filter(line => !isSource(line)).join('\n').trim();
if (!body) return { text: cleaned, empty: true };
const code = Object.hasOwn(IN_THREAD_NEXT, language) ? language : String(language).toLowerCase().split('-')[0];
const next = Object.hasOwn(IN_THREAD_NEXT, code) ? IN_THREAD_NEXT[code] : IN_THREAD_NEXT.en;
return { text: [body, next, ...sources].filter(Boolean).join('\n\n'), empty: false };
}
function expandPipeLists(text) {
return String(text || '')
.split('\n')
.flatMap((line) => {
if (!/=/.test(line) || !/\s\|\s/.test(line)) return [line];
const items = line
.split(/\s*\|\s*/)
.map((part) => part.replace(/^[-*]\s+/, '').trim())
.filter(Boolean);
if (items.length < 2) return [line];
const first = items[0];
const labeled = first.match(/^(.*?:\s*)(.+?\s*=\s*.+)$/);
const rest = items.slice(1).map((item) => `- ${item}`);
if (labeled) {
const intro = labeled[1].trim();
const bullets = [`- ${labeled[2].trim()}`, ...rest];
return intro ? [intro, ...bullets] : bullets;
}
return items.map((item) => `- ${item}`);
})
.join('\n');
}
function formatDiscordReply(text) {
return expandPipeLists(String(text || '').trim())
.replace(/\n{3,}/g, '\n\n')
.trim();
}
const DISCORD_REPLY_MAX = 1900;
// Model output reaches message.reply() verbatim. Allow explicit <@id> user
// pings (see issue: Vector mentions should notify), but never everyone/here
// or role mentions — a crafted question must not turn Vector into a
// mass-ping or role-ping amplifier.
const SAFE_REPLY_MENTIONS = { parse: [], users: [], roles: [], repliedUser: false };
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function mentionNamesForUser(user, member) {
const names = [user?.username, user?.globalName, member?.nickname, member?.displayName]
.map((name) => String(name || '').trim())
.filter((name) => name.length >= 2 && !/\s/.test(name) && !/^everyone$|^here$/i.test(name));
return [...new Set(names)].sort((a, b) => b.length - a.length);
}
function mentionablePeople(message) {
const people = [];
const seen = new Set();
const add = (user, member) => {
const id = String(user?.id || '');
if (!/^\d{5,}$/.test(id) || seen.has(id)) return;
seen.add(id);
people.push({ id, names: mentionNamesForUser(user, member) });
};
add(message?.author, message?.member);
return people;
}
// Discord only pings <@USER_ID>. Plain @username is decoration. Only rewrite
// people we already know (the author, or users they @'d) — never guild-scan.
function rewriteUserMentions(text, message) {
let out = String(text || '');
for (const person of mentionablePeople(message)) {
for (const name of person.names) {
const re = new RegExp(`(^|[^<\\w])@${escapeRegExp(name)}\\b`, 'gi');
out = out.replace(re, `$1<@${person.id}>`);
}
}
const authorId = String(message?.author?.id || '');
out = out.replace(/<@!?(\d+)>/g, (token, id) => (id === authorId ? token : ''));
return out.replace(/[^\S\n]{2,}/g, ' ').trim();
}
function replyMentions(message, { pingAuthor = false, repliedUser = false } = {}) {
const users = [];
const id = String(message?.author?.id || '');
if (pingAuthor && /^\d{5,}$/.test(id)) users.push(id);
return { parse: [], users, roles: [], repliedUser: Boolean(repliedUser) };
}
function isUnknownMessageRef(err) {
const text = `${err?.message || ''} ${err?.code || ''} ${JSON.stringify(err?.rawError || {})}`;
return /Unknown message|MESSAGE_REFERENCE_UNKNOWN_MESSAGE/i.test(text);
}
function wantsAuthorPing(text) {
return /\bping me\b|\bmention me\b|\bnotify me\b|@yourdiscordname/i.test(String(text || ''));
}
function attachAuthorMention(text, message) {
const id = String(message?.author?.id || '');
if (!/^\d{5,}$/.test(id)) return String(text || '');
const out = String(text || '');
if (new RegExp(`<@!?${id}>`).test(out)) return out;
return `<@${id}> ${out}`.trim();
}
function clipThreadHistory(entries, maxEach = 800, maxItems = 24) {
const clipped = (entries || [])
.map((m) => {
const previous = isPreviousPost(m);
const content = clipForDiscord(String(m.content || '').trim(), previous ? 2000 : maxEach);
if (!content) return null;
return { author: m.author, content, ...(previous ? { source: 'previous_post', trust: 'conversation_only' } : {}) };
})
.filter(Boolean);
const previous = clipped.filter(isPreviousPost).slice(0, Math.min(2, Math.max(0, maxItems - 2)));
const current = clipped.filter(entry => !isPreviousPost(entry));
const remaining = Math.max(0, maxItems - previous.length);
if (current.length <= remaining) return [...previous, ...current];
if (!remaining) return previous;
const openingCount = Math.min(2, Math.max(1, remaining - 1));
const tail = remaining - openingCount;
return [...previous, ...current.slice(0,openingCount), ...(tail > 0 ? current.slice(-tail) : [])];
}
function clipForDiscord(text, max = DISCORD_REPLY_MAX) {
const s = String(text || '').trim();
if (s.length <= max) return s;
return `${s.slice(0, Math.max(0, max - 1))}…`;
}
const ESCALATE_FOOTER =
'A person on the team needs to take this. I have not pinged anyone yet.';
const FAILED_HANDOFF_FOOTER =
'I could not deliver this to the support team from here. Please email help@omi.me with the details; do not post order or account information publicly.';
const PINGED_FOOTER = 'A person on the team has this now.';
const THREAD_REPLY_FOOTER = 'A person on the team has this now and will reply in this thread.';
const DUPLICATE_FOOTER =
'A person on the team already has this. I have not sent another ping.';
const PING_NARRATION = [
/have not (pinged|messaged|contacted|notified)/i,
/noch niemanden/i,
/not able to ping/i,
/can'?t ping/i,
/cannot ping/i,
/won'?t tell you i (did|pinged)/i,
/will not tell you i (did|pinged)/i,
/ping anyone myself/i,
/handoff happens/i,
/flagging this/i,
/i('m| am) not able to ping/i,
/going into your account/i,
/can'?t send you a mention/i,
/cannot send you a mention/i,
/send you a mention myself/i,
/i('m| am) passing on/i,
/keep re-explaining/i,
/sent this along/i,
/passed this along/i,
/passing it along/i,
];
// Retain only a complete, bounded requirement expression here. An arbitrary
// suffix (including another predicate about the same person) is not a proven
// requirement. Unknown staff-affiliation claims keep the old fail-closed guard.
// This is intentionally not a general semantic parser. Mixed/unknown removed
// units belong in the untrusted reviewer audit, never in an inferred rewrite.
function isPingNarration(text) {
// Classify displayed words without allowing Markdown to hide a predicate;
// the original text, conditions and punctuation are retained when allowed.
const shown = String(text || '').replace(/[*_~`]/g, '').trim();
if (PING_NARRATION.some(re => re.test(shown))) return true;
return /person on the team/i.test(shown) && !classifyReviewRequirement(shown).recognized;
}
const ALREADY_SAID_PINGED = [
/sent this to a person/i,
/posted this to the team/i,
/person on the team has this now/i,
];
function dropPingNarration(text) {
return String(text || '')
.replace(/\bi can('t|not) send you a mention( myself)?\b/gi, '')
.replace(/\bi('m| am) passing on\b/gi, '')
.replace(/\bi('m| am) passing it along( as-is)?\b/gi, '')
.replace(/\byou don'?t need to keep re-explaining\b/gi, '')
.split('\n')
.map((line) => {
if (!isPingNarration(line)) return line;
return line
// A dash is not proof of semantic independence: deleting an approval
// condition before it can leave a stronger, unsafe instruction.
.split(/(?<=[.!?])\s+/)
.filter((sentence) => !isPingNarration(sentence))
.join(' ');
})
.map((line) => line.replace(/^[,\s]+/, '').replace(/\s{2,}/g, ' ').trim())
.filter(Boolean)
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
// Claims that a bug is already fixed, solved, or shipped, plus improvised repair steps.
// "It has not shipped." stays — that sentence is the honest status.
// "I am not sure" / "Ich bin mir nicht sicher" stay — those are not a diagnosis.
// "Não tenho certeza." / "No estoy seguro." stay — they do not match the fix claims below.
// "I am sure" does not match "I am not sure": "not" sits between "am" and "sure".
// Recordings location and "firmware bug" stay in honesty.js.
const FALSE_CERTAINTY = [
/\bhalf[-\s]+solved\b/i,
/\balready\s+solved\b/i,
/\balready\s+fixed\b/i,
/\bthis\s+is\s+fixed\b/i,
/\bthis\s+is\s+solved\b/i,
/\bit(?:['’]s|\s+is)\s+fixed\b/i,
/\bit(?:['’]s|\s+is)\s+solved\b/i,
/\b(?:bug|issue|problem)\s+is\s+(?:fixed|solved)\b/i,
/\bhas\s+been\s+fixed\b/i,
/\bhas\s+been\s+solved\b/i,
/\bjá\s+foi\s+corrigido\b/i,
/\bjá\s+está\s+resolvido\b/i,
/\bya\s+está\s+arreglado\b/i,
/\besto\s+está\s+solucionado\b/i,
/\bschon\s+behoben\b/i,
/\bbereits\s+gelöst\b/i,
/\b(?:do\s+not|don['’]?t)\s+keep\s+turning\s+it\s+on\b/i,
/\bset\s+it\s+aside\b/i,
/\bleave\s+it\s+as\s+it\s+is\b/i,
/\bstop\s+turning\s+it\s+on\b/i,
/\bI(?:['’]m|\s+am)\s+sure\b/i,
/\bthis\s+will\s+fix\s+it\b/i,
];
function claimsBugShipped(sentence) {
const s = String(sentence || '')
.replace(/\b(?:not|never|cannot)(?:\s+\w+){0,3}\s+shipped\b/gi, '')
.replace(/n['’]t(?:\s+\w+){0,3}\s+shipped\b/gi, '');
return /\bshipped\b/i.test(s);
}
function isHonestUncertainty(sentence) {
const s = String(sentence || '');
return /\bI(?:['’]m|\s+am)\s+not\s+sure\b/i.test(s) || /\bnicht\s+sicher\b/i.test(s);
}
// "kein bekannter Fix" is the negation. Drop only when a positive known-fix claim remains.
function claimsKnownFix(sentence) {
const s = String(sentence || '')
.replace(/\bno\s+known\s+fix\b/gi, ' ')
.replace(/\bkein(?:e[mnrs]?)?\s+bekannt(?:e[rsn]?)?\s+fix(?:es|e)?\b/gi, ' ');
return /\bknown\s+fix\b/i.test(s) || /\bbekannt(?:e[rsn]?)?\s+fix(?:es|e)?\b/i.test(s);
}
function looksLikeFalseCertainty(sentence) {
const s = String(sentence || '');
if (FALSE_CERTAINTY.some((re) => re.test(s))) return true;
if (claimsBugShipped(s)) return true;
if (claimsKnownFix(s)) return true;
if (isHonestUncertainty(s)) return false;
if (/\bthe\s+cause\s+is\b/i.test(s)) return true;
return /\bno\s+known\s+fix\b/i.test(s);
}
function stripFalseCertainty(text) {
return String(text || '')
.split('\n')
.map((line) =>
line
.split(/(?<=[.!?])\s+/)
.filter((sentence) => sentence.trim() && !looksLikeFalseCertainty(sentence))
.join(' ')
)
.map((line) => line.replace(/^[,\s]+/, '').replace(/\s{2,}/g, ' ').trim())
.filter(Boolean)
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
const ISSUE_FOOTER = 'The problem is written in this thread. Keep talking here — you do not need to ping anyone.';
function escalateReply(answer, opts = {}) {
recordStage('handoff_input', answer);
const shown = composeEscalation(answer, opts);
return recordStage('handoff_output', require('./publicReplyPrivacy').publicReplyPrivacy(shown, {
...opts, orderAvailable: opts.orderAvailable ?? require('./orderFlow').isLive(),
staffDelivered: opts.pinged === true && !opts.deliveryFailed,
}).text);
}
function composeEscalation(answer, opts = {}) {
const pinged = Boolean(opts.pinged);
const duplicate = Boolean(opts.duplicate);
const footers = opts.footers;
let body = dropPingNarration(String(answer || '').trim());
if (pinged || opts.deliveryFailed) body = stripSupportRedirect(body, {language:opts.language || 'en'});
if (opts.deliveryFailed) {
return [body, footers?.failed || FAILED_HANDOFF_FOOTER].filter(Boolean).join('\n\n');
}
if (pinged && opts.replyInThread) {
return [body, footers?.thread || THREAD_REPLY_FOOTER].filter(Boolean).join('\n\n');
}
if (opts.conversation) {
return body;
}
if (opts.issue && !opts.pingAuthor) {
if (/written (up|in this thread)/i.test(body)) return body;
return [body, footers?.issue || ISSUE_FOOTER].filter(Boolean).join('\n\n');
}
if (pinged) {
const footer = duplicate
? (footers?.duplicate || DUPLICATE_FOOTER)
: (footers?.sent || PINGED_FOOTER);
if (!duplicate && ALREADY_SAID_PINGED.some((re) => re.test(body))) return body;
if (duplicate && /already has this/i.test(body)) return body;
return [body, footer].filter(Boolean).join('\n\n');
}
if (opts.pingAuthor) return body;
return [body, footers?.pending || ESCALATE_FOOTER].filter(Boolean).join('\n\n');
}
module.exports = {
isOnCooldown,
markReplied,
wasRecentlyAnsweredText,
markAnsweredText,
claimMessage,
claimAsker,
releaseAsker,
pruneTracked,
containsEscalationKeyword,
shouldEscalate,
typingDelay,
sanitizeReply,
stripSupportRedirect,
stripSupportRedirectWithAudit,
inThreadReply,
formatDiscordReply,
clipForDiscord,
DISCORD_REPLY_MAX,
SAFE_REPLY_MENTIONS,
rewriteUserMentions,
wantsAuthorPing,
attachAuthorMention,
replyMentions,
isUnknownMessageRef,
clipThreadHistory,
escalateReply,
ESCALATE_FOOTER,
PINGED_FOOTER,
DUPLICATE_FOOTER,
ISSUE_FOOTER,
CONFIDENCE_THRESHOLD,
needsHumanAccess,
stripPingNarration: dropPingNarration,
stripFalseCertainty,
};