Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion skills/meet-join/__tests__/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ import { describe, expect, test } from "bun:test";

import {
DEFAULT_MEET_OBJECTION_KEYWORDS,
DEFAULT_MEET_PROACTIVE_CHAT_KEYWORDS,
MeetServiceSchema,
} from "../config-schema.js";

const DEFAULT_PROACTIVE_CHAT = {
enabled: true,
detectorKeywords: [...DEFAULT_MEET_PROACTIVE_CHAT_KEYWORDS],
tier2DebounceMs: 5_000,
escalationCooldownSec: 30,
tier2MaxTranscriptSec: 30,
};

describe("MeetServiceSchema", () => {
test("empty object parses to the documented defaults (feature off by default)", () => {
const parsed = MeetServiceSchema.parse({});
Expand All @@ -18,6 +27,7 @@ describe("MeetServiceSchema", () => {
objectionKeywords: [...DEFAULT_MEET_OBJECTION_KEYWORDS],
dockerNetwork: "bridge",
maxMeetingMinutes: 240,
proactiveChat: DEFAULT_PROACTIVE_CHAT,
});
});

Expand Down Expand Up @@ -46,7 +56,7 @@ describe("MeetServiceSchema", () => {
maxMeetingMinutes: 60,
};
const parsed = MeetServiceSchema.parse(input);
expect(parsed).toEqual(input);
expect(parsed).toEqual({ ...input, proactiveChat: DEFAULT_PROACTIVE_CHAT });
});

test("rejects negative maxMeetingMinutes", () => {
Expand Down
92 changes: 92 additions & 0 deletions skills/meet-join/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ export const DEFAULT_MEET_OBJECTION_KEYWORDS: readonly string[] = [
"don't want this recorded",
];

/**
* Default Tier 1 regex keyword patterns for the proactive-chat opportunity
* detector. Each entry is compiled as a case-insensitive {@link RegExp} at
* runtime. Patterns are intentionally broad — false positives only trigger
* a Tier 2 LLM confirmation, so we bias toward coverage.
*/
export const DEFAULT_MEET_PROACTIVE_CHAT_KEYWORDS: readonly string[] = [
// Direct "can you / could you / would you / will you" requests
"\\b(can|could|would|will)\\s+you\\b",
// Collective requests addressed to anyone in the meeting
"\\bcan\\s+(anyone|someone)\\b",
"\\bdoes\\s+(anyone|someone)\\s+know\\b",
"\\banyone\\s+(have|know)\\b",
];

/**
* Normalize `joinName` — coerce empty or whitespace-only strings to `null` so
* downstream code only has to check for `null` when deciding whether to fall
Expand Down Expand Up @@ -110,6 +125,83 @@ export const MeetServiceSchema = z
.describe(
"Hard ceiling in minutes — the bot container is killed once this elapses, regardless of meeting state",
),
proactiveChat: z
.object({
enabled: z
.boolean({
error: "services.meet.proactiveChat.enabled must be a boolean",
})
.default(true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 proactiveChat.enabled defaults to true — behavioral change for existing Meet users when wired

The proactiveChat.enabled field defaults to true (skills/meet-join/config-schema.ts:134). When PR 7 wires the detector into the session manager, existing users who have services.meet.enabled: true will have proactive chat automatically enabled without explicit opt-in. Currently this is inert (no production code instantiates MeetChatOpportunityDetector), so there's no immediate impact. However, the wiring PR should either: (a) keep this default and document it in UPDATES.md, or (b) change the default to false so proactive chat is opt-in. The choice depends on product intent — flagging for the wiring PR author to decide.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

.describe(
"Whether the assistant proactively watches meeting transcript and chat for opportunities to respond via meeting chat.",
),
detectorKeywords: z
.array(
z.string({
error:
"services.meet.proactiveChat.detectorKeywords values must be strings",
}),
)
.default([...DEFAULT_MEET_PROACTIVE_CHAT_KEYWORDS])
.describe(
"Tier 1 regex patterns (case-insensitive) that trigger a Tier 2 LLM confirmation when matched against transcript or chat text.",
),
tier2DebounceMs: z
.number({
error:
"services.meet.proactiveChat.tier2DebounceMs must be a number",
})
.int(
"services.meet.proactiveChat.tier2DebounceMs must be an integer",
)
.nonnegative(
"services.meet.proactiveChat.tier2DebounceMs must be non-negative",
)
.default(5_000)
.describe(
"Minimum milliseconds between consecutive Tier 2 LLM calls. Tier 1 hits arriving within this window are collapsed into a single LLM call.",
),
escalationCooldownSec: z
.number({
error:
"services.meet.proactiveChat.escalationCooldownSec must be a number",
})
.int(
"services.meet.proactiveChat.escalationCooldownSec must be an integer",
)
.nonnegative(
"services.meet.proactiveChat.escalationCooldownSec must be non-negative",
)
.default(30)
.describe(
"Seconds between consecutive positive escalations. A Tier 2 positive verdict arriving within this window of the previous escalation is suppressed.",
),
tier2MaxTranscriptSec: z
.number({
error:
"services.meet.proactiveChat.tier2MaxTranscriptSec must be a number",
})
.int(
"services.meet.proactiveChat.tier2MaxTranscriptSec must be an integer",
)
.positive(
"services.meet.proactiveChat.tier2MaxTranscriptSec must be positive",
)
.default(30)
.describe(
"Rolling transcript window (seconds) included in the Tier 2 LLM prompt.",
),
})
.default({
enabled: true,
detectorKeywords: [...DEFAULT_MEET_PROACTIVE_CHAT_KEYWORDS],
tier2DebounceMs: 5_000,
escalationCooldownSec: 30,
tier2MaxTranscriptSec: 30,
})
.describe(
"Proactive-chat opportunity detector tuning. The detector uses a Tier 1 regex fast filter plus a Tier 2 LLM confirmation before the assistant posts in meeting chat.",
),
})
.describe(
"Google Meet bot configuration — controls the containerized Meet joining bot, consent messaging, and objection handling",
Expand Down
Loading
Loading