feat(notifications): emit at maintenance/turn/sync/reauth events

This commit is contained in:
Gabriel Brown
2026-07-11 16:08:22 -04:00
parent 4eb0c963ff
commit 3766a29871
7 changed files with 270 additions and 2 deletions
+27
View File
@@ -4,6 +4,7 @@ import type { Doc, Id } from './_generated/dataModel';
import type { MutationCtx } from './_generated/server';
import { internalMutation, mutation, query } from './_generated/server';
import { getOwnedSpoon, getRequiredUserId, optionalText } from './model';
import { emitNotification } from './notifications';
import type { AgentRuntimeName } from './runtimeSupport';
import { runtimesForProfile } from './runtimeSupport';
import { assertWorkerToken } from './workerAuth';
@@ -1344,6 +1345,21 @@ export const updateStatus = mutation({
}
await ctx.db.patch(job.threadId, threadPatch);
}
if (
(args.status === 'changes_ready' ||
args.status === 'draft_pr_opened') &&
job.status !== args.status
) {
await emitNotification(ctx, {
userId: job.ownerId,
kind: 'agent_turn_finished',
title: 'Agent turn finished',
body: args.summary ?? job.summary ?? '',
link: `/threads/${job.threadId}`,
threadId: job.threadId,
spoonId: job.spoonId,
});
}
}
return { success: true };
},
@@ -1687,6 +1703,17 @@ export const applyMaintenanceDecision = mutation({
createdAt: now,
updatedAt: now,
});
if (status === 'waiting_for_user') {
await emitNotification(ctx, {
userId: job.ownerId,
kind: 'agent_needs_input',
title: 'Agent needs your input',
body: args.summary,
link: `/threads/${job.threadId}`,
threadId: job.threadId,
spoonId: job.spoonId,
});
}
return { success: true };
},
});
+10
View File
@@ -8,6 +8,7 @@ import {
query,
} from './_generated/server';
import { getRequiredUserId } from './model';
import { emitNotification } from './notifications';
export const getInstallUrl = query({
args: {},
@@ -158,6 +159,15 @@ export const setConnectionStatusByInstallation = internalMutation({
const now = Date.now();
for (const connection of connections) {
await ctx.db.patch(connection._id, { status, updatedAt: now });
if (status !== 'active') {
await emitNotification(ctx, {
userId: connection.userId,
kind: 'connection_needs_reauth',
title: 'GitHub connection needs re-authorization',
body: 'Reconnect your GitHub account to keep syncing.',
link: '/settings/integrations',
});
}
}
return { updated: connections.length };
+10
View File
@@ -378,6 +378,16 @@ const refreshOwnedSpoon = async (
error: message,
}),
]);
if (!rateLimited) {
await ctx.runMutation(internal.notifications.emit, {
userId: ownerId,
kind: 'sync_failed',
title: `Sync failed for ${spoon.name}`,
body: message,
link: `/spoons/${spoonId}`,
spoonId,
});
}
throw new ConvexError(message);
}
};
+10
View File
@@ -15,6 +15,7 @@ import {
optionalText,
requireText,
} from './model';
import { emitNotification } from './notifications';
const threadSource = v.union(
v.literal('user_request'),
@@ -500,6 +501,15 @@ export const createMaintenanceThread = internalMutation({
jobType: args.jobType,
},
);
await emitNotification(ctx, {
userId: args.ownerId,
kind: 'maintenance_thread',
title: args.title,
body: args.summary,
link: `/threads/${threadId}`,
spoonId: args.spoonId,
threadId,
});
return threadId;
},
});