@chatpanel/events 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -1
- package/package.json +1 -1
- package/voice-intents.js +55 -4
package/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export {
|
|
|
59
59
|
compileWake, findWakeCommand, parseCommand, commandsFromSegments,
|
|
60
60
|
parseDuration, parseClock, parseWhen, parseNumberWords, normalizeSpeech, tokenize, editDistance,
|
|
61
61
|
defineVoiceIntent, createVoiceIntentRegistry, defaultVoiceIntents, BUILTIN_VOICE_INTENTS,
|
|
62
|
-
timerIntent, reminderIntent, noteIntent, monitorIntent,
|
|
62
|
+
timerIntent, reminderIntent, scheduleIntent, noteIntent, monitorIntent,
|
|
63
63
|
} from './voice-intents.js';
|
|
64
64
|
export { explainMcpError, packageFromArgs } from './mcp-errors.js';
|
|
65
65
|
export { createManifest, ManifestError, SOURCES } from './manifest.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/voice-intents.js
CHANGED
|
@@ -538,7 +538,37 @@ export const monitorIntent = defineVoiceIntent({
|
|
|
538
538
|
},
|
|
539
539
|
});
|
|
540
540
|
|
|
541
|
-
|
|
541
|
+
// "Every weekday at 8am run my daily brief." The recurrence parser already existed for
|
|
542
|
+
// reminders; what makes this different is that the thing being scheduled is WORK — a skill
|
|
543
|
+
// the user already wrote — so the job says only when, and the skill stays the single
|
|
544
|
+
// definition of what. Declared class C because it will start a model turn every time.
|
|
545
|
+
export const scheduleIntent = defineVoiceIntent({
|
|
546
|
+
id: 'voice:schedule',
|
|
547
|
+
label: 'Schedule something',
|
|
548
|
+
description: 'Runs one of your skills (or a plain instruction) on a schedule.',
|
|
549
|
+
examples: ['every weekday at 8am run my daily brief', 'run the standup summary every morning', 'tomorrow at 9 do the release checklist'],
|
|
550
|
+
classUsed: 'C',
|
|
551
|
+
match: (command, { now = Date.now() } = {}) => {
|
|
552
|
+
const verb = /\b(run|do|start|kick\s+off|execute)\b/i.exec(command);
|
|
553
|
+
if (!verb) return null;
|
|
554
|
+
const when = parseWhen(command, { now });
|
|
555
|
+
// No time is not a schedule — it is a request to do something now, which is a chat
|
|
556
|
+
// message, not a job. Refusing here is what keeps "run the checklist" out of the
|
|
557
|
+
// scheduler.
|
|
558
|
+
if (!when) return null;
|
|
559
|
+
let target = when.end > when.start
|
|
560
|
+
? command.slice(0, when.start) + ' ' + command.slice(when.end)
|
|
561
|
+
: command;
|
|
562
|
+
const v = /\b(run|do|start|kick\s+off|execute)\b/i.exec(target);
|
|
563
|
+
target = tidy((v ? target.slice(v.index + v[0].length) : target)
|
|
564
|
+
.replace(/^\s*(?:my|the|our)\b/i, '')
|
|
565
|
+
.replace(/\b(skill|job|task)\b\s*$/i, ''));
|
|
566
|
+
if (!target) return null;
|
|
567
|
+
return { target, at: when.at, recurrence: when.recurrence, when: when.kind };
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
export const BUILTIN_VOICE_INTENTS = Object.freeze([timerIntent, reminderIntent, scheduleIntent, noteIntent, monitorIntent]);
|
|
542
572
|
|
|
543
573
|
function tidy(s) {
|
|
544
574
|
return String(s || '').replace(/\s+/g, ' ').replace(/^[\s,.:;-]+|[\s,.:;-]+$/g, '').trim();
|
|
@@ -596,6 +626,15 @@ export function commandsFromSegments(segments, {
|
|
|
596
626
|
if (seg.t && seg.t <= sinceTs) continue;
|
|
597
627
|
const parsed = parseCommand(seg.text, { wake, intents, now });
|
|
598
628
|
if (!parsed) continue;
|
|
629
|
+
// NO INTENT, NO ACTION. parseCommand returns a shape for anything that carries the wake
|
|
630
|
+
// word and a time-ish phrase, intent included or not — so "we should talk about the chat
|
|
631
|
+
// panel roadmap next week" came back as a command with intent:null and the caller acted
|
|
632
|
+
// on it anyway. In a live meeting that means ordinary conversation quietly sets timers,
|
|
633
|
+
// which is what happened: a caption grows, keeps matching, and fires again.
|
|
634
|
+
//
|
|
635
|
+
// An automation that runs when it did not understand the request is worse than one that
|
|
636
|
+
// does nothing, so an unrecognised utterance stops here.
|
|
637
|
+
if (!parsed.intent) continue;
|
|
599
638
|
const allowed = isSelf ? !!isSelf(seg.speaker) : false;
|
|
600
639
|
out.push({
|
|
601
640
|
...parsed,
|
|
@@ -603,9 +642,21 @@ export function commandsFromSegments(segments, {
|
|
|
603
642
|
speaker: seg.speaker || '',
|
|
604
643
|
t: seg.t || now,
|
|
605
644
|
meetingId,
|
|
606
|
-
// Stable across redeliveries of the same segment, so the
|
|
607
|
-
//
|
|
608
|
-
|
|
645
|
+
// Stable across redeliveries of the same segment, so the dedupe actually dedupes.
|
|
646
|
+
//
|
|
647
|
+
// `parsed.at` used to be in this key, and it is an ABSOLUTE time computed as now + the
|
|
648
|
+
// spoken duration — so it changed on every scan. A live caption is rescanned as the
|
|
649
|
+
// sentence grows (deliberately: a half-heard command must get a second chance), which
|
|
650
|
+
// meant one "set a timer for 10 seconds" produced a brand-new key, and a brand-new
|
|
651
|
+
// timer, on every caption update — indefinitely, and faster than the user could delete
|
|
652
|
+
// them. The key now carries only what the same utterance keeps: where it was said, and
|
|
653
|
+
// what it asked for.
|
|
654
|
+
// IDENTITY, NOT FRESHNESS. `seg.t` is bumped every time a live caption's text grows —
|
|
655
|
+
// that is what keeps the line flowing through the delta filter — so keying on it made
|
|
656
|
+
// one spoken request look like a new request on every update, and a single "set a timer
|
|
657
|
+
// for 30 seconds" became a screenful of timers. `sid` is assigned once per utterance and
|
|
658
|
+
// never moves, so the same sentence keeps one key however many times it is rescanned.
|
|
659
|
+
key: `voice:${meetingId}:${seg.sid || seg.t || 0}:${parsed.intent || 'unknown'}:${parsed.ms ?? parsed.when ?? ''}`,
|
|
609
660
|
});
|
|
610
661
|
if (out.length >= max) break; // a pathological transcript cannot fire fifty actions
|
|
611
662
|
}
|