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
5 changes: 5 additions & 0 deletions assistant/src/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,11 @@ export class DaemonServer {
// DB, exposing only the narrow surface the wake helper needs.
registerDefaultWakeResolver(async (conversationId) => {
try {
// Only resolve existing conversations — don't create ghost
// conversations for stale targets (e.g. meetings that ended
// but a delayed opportunity callback still fires).
const existing = getConversation(conversationId);
if (!existing) return null;
const conversation = await this.getOrCreateConversation(conversationId);
return conversationToWakeTarget(conversation);
} catch (err) {
Expand Down
4 changes: 4 additions & 0 deletions assistant/src/providers/ratelimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const log = getLogger("rate-limit");
export class RateLimitProvider implements Provider {
public readonly name: string;

get tokenEstimationProvider(): string | undefined {
return this.inner.tokenEstimationProvider;
}

private requestTimestamps: number[];

constructor(
Expand Down
4 changes: 4 additions & 0 deletions assistant/src/providers/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ function normalizeSendMessageOptions(
export class RetryProvider implements Provider {
public readonly name: string;

get tokenEstimationProvider(): string | undefined {
return this.inner.tokenEstimationProvider;
}

constructor(private readonly inner: Provider) {
this.name = inner.name;
}
Expand Down
14 changes: 10 additions & 4 deletions assistant/src/runtime/agent-wake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,12 @@ async function waitUntilIdle(
target: WakeTarget,
nowFn: () => number,
timeoutMs = 30_000,
): Promise<void> {
): Promise<boolean> {
const deadline = nowFn() + timeoutMs;
// 50ms backoff is fine — wakes are not latency-critical and a user turn
// typically completes on the order of seconds.
while (target.isProcessing() && nowFn() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
return !target.isProcessing();
}

/**
Expand Down Expand Up @@ -315,7 +314,14 @@ export async function wakeAgentForOpportunity(
return { invoked: false, producedToolCalls: false };
}

await waitUntilIdle(target, nowFn);
const idle = await waitUntilIdle(target, nowFn);
if (!idle) {
log.warn(
{ conversationId, source },
"agent-wake: conversation still processing after timeout; skipping",
);
return { invoked: false, producedToolCalls: false };
Comment thread
siddseethepalli marked this conversation as resolved.
}

const baseline = target.getMessages();
const hintContent = `[opportunity:${source}] ${hint}`;
Expand Down
18 changes: 18 additions & 0 deletions skills/meet-join/bot/src/control/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ export function createHttpServer(
*/
let playbackChain: Promise<void> = Promise.resolve();

/**
* Tail of the chat-send queue. Concurrent POST /send_chat requests must
* not interleave Playwright operations on the shared chat input — one
* fill()/press() sequence must complete before the next begins, otherwise
* two messages race on the same DOM element and both may be lost or
* garbled. Identical pattern to `playbackChain` above.
*/
let chatChain: Promise<void> = Promise.resolve();

const app = new Hono();

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -232,11 +241,20 @@ export function createHttpServer(
400,
);
}
const previousChat = chatChain;
let releaseChatChain!: () => void;
chatChain = new Promise<void>((resolve) => {
releaseChatChain = resolve;
});
await previousChat;

try {
await onSendChat(parsed.data.text);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return c.json({ sent: false, error: message }, 502);
} finally {
releaseChatChain();
}
return c.json({ sent: true, timestamp: new Date().toISOString() }, 200);
});
Expand Down
4 changes: 2 additions & 2 deletions skills/meet-join/daemon/__tests__/proactive-chat-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,8 @@ describe("proactive-chat E2E — Tier 1 hit → Tier 2 confirms → agent wake
expect(blocks[0]!.type).toBe("tool_use");
expect(blocks[0]!.name).toBe("meet_send_chat");

// Performance envelope — comfortable headroom over the plan's 100ms.
expect(elapsedMs).toBeLessThan(100);
// Performance envelope — generous headroom for CI runners.
expect(elapsedMs).toBeLessThan(2000);
Comment thread
siddseethepalli marked this conversation as resolved.

detector.dispose();
} finally {
Expand Down
3 changes: 3 additions & 0 deletions skills/meet-join/daemon/chat-opportunity-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export class MeetChatOpportunityDetector {
private readonly now: () => number;

private unsubscribe: MeetEventUnsubscribe | null = null;
private disposed = false;

/** Compiled Tier 1 regexes. Empty when `config.enabled === false`. */
private readonly patterns: RegExp[];
Expand Down Expand Up @@ -234,6 +235,7 @@ export class MeetChatOpportunityDetector {
* vocabulary ("dispose") called out in the phase plan.
*/
dispose(): void {
this.disposed = true;
if (this.unsubscribe) {
try {
this.unsubscribe();
Expand Down Expand Up @@ -410,6 +412,7 @@ export class MeetChatOpportunityDetector {
const prompt = this.buildPrompt(triggerReason, triggerText);
try {
const decision = await this.callDetectorLLM(prompt);
if (this.disposed) return;
if (!decision.shouldRespond) {
log.debug(
{
Expand Down
Loading