@yattalo/task-system 0.4.0 → 0.5.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.
Files changed (57) hide show
  1. package/README.md +48 -0
  2. package/dashboard-app/assets/spa-entry-CnIKatv4.js +24 -0
  3. package/dashboard-app/assets/styles-CAIFwsCh.css +1 -0
  4. package/dashboard-app/index.html +14 -0
  5. package/dist/commands/dashboard.d.ts +2 -0
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/dashboard.js +133 -6
  8. package/dist/commands/dashboard.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +35 -1
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/generators/mgrep-setup.d.ts +6 -0
  13. package/dist/generators/mgrep-setup.d.ts.map +1 -0
  14. package/dist/generators/mgrep-setup.js +191 -0
  15. package/dist/generators/mgrep-setup.js.map +1 -0
  16. package/dist/generators/mgrep-skill.d.ts +6 -0
  17. package/dist/generators/mgrep-skill.d.ts.map +1 -0
  18. package/dist/generators/mgrep-skill.js +173 -0
  19. package/dist/generators/mgrep-skill.js.map +1 -0
  20. package/dist/generators/uca-functions.d.ts +8 -0
  21. package/dist/generators/uca-functions.d.ts.map +1 -0
  22. package/dist/generators/uca-functions.js +57 -0
  23. package/dist/generators/uca-functions.js.map +1 -0
  24. package/dist/generators/uca-reexports.d.ts +8 -0
  25. package/dist/generators/uca-reexports.d.ts.map +1 -0
  26. package/dist/generators/uca-reexports.js +112 -0
  27. package/dist/generators/uca-reexports.js.map +1 -0
  28. package/dist/generators/uca-schema.d.ts +8 -0
  29. package/dist/generators/uca-schema.d.ts.map +1 -0
  30. package/dist/generators/uca-schema.js +650 -0
  31. package/dist/generators/uca-schema.js.map +1 -0
  32. package/dist/index.js +3 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/presets/research.d.ts.map +1 -1
  35. package/dist/presets/research.js +10 -0
  36. package/dist/presets/research.js.map +1 -1
  37. package/dist/presets/software.d.ts.map +1 -1
  38. package/dist/presets/software.js +10 -0
  39. package/dist/presets/software.js.map +1 -1
  40. package/dist/utils/detect.d.ts.map +1 -1
  41. package/dist/utils/detect.js +15 -0
  42. package/dist/utils/detect.js.map +1 -1
  43. package/dist/utils/merge.d.ts.map +1 -1
  44. package/dist/utils/merge.js +2 -0
  45. package/dist/utils/merge.js.map +1 -1
  46. package/package.json +5 -3
  47. package/templates/uca/agents.ts +59 -0
  48. package/templates/uca/contextEntries.ts +125 -0
  49. package/templates/uca/cronManager.ts +255 -0
  50. package/templates/uca/cronUtils.ts +99 -0
  51. package/templates/uca/driftEvents.ts +106 -0
  52. package/templates/uca/heartbeats.ts +167 -0
  53. package/templates/uca/hooks.ts +430 -0
  54. package/templates/uca/memory.ts +326 -0
  55. package/templates/uca/sessionBridge.ts +238 -0
  56. package/templates/uca/skills.ts +284 -0
  57. package/templates/uca/ucaTasks.ts +500 -0
@@ -0,0 +1,326 @@
1
+ import { mutation, query } from "../_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ const memoryTierValidator = v.union(v.literal("hot"), v.literal("warm"), v.literal("cold"));
5
+
6
+ function makeId(prefix: string): string {
7
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8
+ }
9
+
10
+ function estimateTokens(text: string): number {
11
+ return Math.max(1, Math.ceil(text.trim().split(/\s+/).length * 1.3));
12
+ }
13
+
14
+ function tierToContextTier(tier: "hot" | "warm" | "cold"): "draft" | "reviewed" | "curated" {
15
+ if (tier === "hot") return "draft";
16
+ if (tier === "warm") return "reviewed";
17
+ return "curated";
18
+ }
19
+
20
+ function cosineSimilarity(a: number[], b: number[]): number {
21
+ if (a.length === 0 || b.length === 0 || a.length !== b.length) return 0;
22
+ let dot = 0;
23
+ let normA = 0;
24
+ let normB = 0;
25
+ for (let i = 0; i < a.length; i += 1) {
26
+ dot += a[i]! * b[i]!;
27
+ normA += a[i]! * a[i]!;
28
+ normB += b[i]! * b[i]!;
29
+ }
30
+ if (normA === 0 || normB === 0) return 0;
31
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
32
+ }
33
+
34
+ function lexicalScore(haystack: string, queryText: string): number {
35
+ if (!queryText) return 0;
36
+ const lowerHaystack = haystack.toLowerCase();
37
+ const lowerQuery = queryText.toLowerCase();
38
+ if (lowerHaystack.includes(lowerQuery)) return 1;
39
+
40
+ const tokens = lowerQuery.split(/\s+/).filter(Boolean);
41
+ if (tokens.length === 0) return 0;
42
+ let matched = 0;
43
+ for (const token of tokens) {
44
+ if (lowerHaystack.includes(token)) matched += 1;
45
+ }
46
+ return matched / tokens.length;
47
+ }
48
+
49
+ export const listMemory = query({
50
+ args: {
51
+ agentId: v.optional(v.string()),
52
+ tier: v.optional(memoryTierValidator),
53
+ taskId: v.optional(v.string()),
54
+ limit: v.optional(v.number()),
55
+ },
56
+ handler: async (ctx, args) => {
57
+ let items = await ctx.db.query("agentMemory").collect();
58
+ if (args.agentId) items = items.filter((item) => item.agentId === args.agentId);
59
+ if (args.tier) items = items.filter((item) => item.tier === args.tier);
60
+ if (args.taskId) items = items.filter((item) => item.taskId === args.taskId);
61
+ items.sort((a, b) => b.updatedAt - a.updatedAt);
62
+ return args.limit ? items.slice(0, args.limit) : items;
63
+ },
64
+ });
65
+
66
+ export const memoryStats = query({
67
+ args: { agentId: v.optional(v.string()) },
68
+ handler: async (ctx, { agentId }) => {
69
+ let items = await ctx.db.query("agentMemory").collect();
70
+ if (agentId) items = items.filter((item) => item.agentId === agentId);
71
+
72
+ const byTier: Record<string, { count: number; tokens: number }> = {
73
+ hot: { count: 0, tokens: 0 },
74
+ warm: { count: 0, tokens: 0 },
75
+ cold: { count: 0, tokens: 0 },
76
+ };
77
+
78
+ for (const item of items) {
79
+ byTier[item.tier].count += 1;
80
+ byTier[item.tier].tokens += item.tokens;
81
+ }
82
+
83
+ return {
84
+ total: items.length,
85
+ totalTokens: items.reduce((sum, item) => sum + item.tokens, 0),
86
+ byTier,
87
+ };
88
+ },
89
+ });
90
+
91
+ export const addMemory = mutation({
92
+ args: {
93
+ memoryId: v.optional(v.string()),
94
+ agentId: v.string(),
95
+ taskId: v.optional(v.string()),
96
+ tier: memoryTierValidator,
97
+ content: v.string(),
98
+ summary: v.optional(v.string()),
99
+ tokens: v.optional(v.number()),
100
+ embedding: v.optional(v.array(v.number())),
101
+ tags: v.optional(v.array(v.string())),
102
+ linkedContextEntryId: v.optional(v.string()),
103
+ expiresAt: v.optional(v.number()),
104
+ },
105
+ handler: async (ctx, args) => {
106
+ const now = Date.now();
107
+ const memoryId = args.memoryId ?? makeId("mem");
108
+
109
+ const existing = await ctx.db
110
+ .query("agentMemory")
111
+ .withIndex("by_memoryId", (q: any) => q.eq("memoryId", memoryId))
112
+ .first();
113
+ if (existing) {
114
+ await ctx.db.patch(existing._id, {
115
+ agentId: args.agentId,
116
+ taskId: args.taskId,
117
+ tier: args.tier,
118
+ content: args.content,
119
+ summary: args.summary,
120
+ tokens: args.tokens ?? estimateTokens(args.content),
121
+ embedding: args.embedding,
122
+ tags: args.tags ?? existing.tags,
123
+ linkedContextEntryId: args.linkedContextEntryId,
124
+ expiresAt: args.expiresAt,
125
+ updatedAt: now,
126
+ });
127
+ return { memoryId, updated: true };
128
+ }
129
+
130
+ await ctx.db.insert("agentMemory", {
131
+ memoryId,
132
+ agentId: args.agentId,
133
+ taskId: args.taskId,
134
+ tier: args.tier,
135
+ content: args.content,
136
+ summary: args.summary,
137
+ tokens: args.tokens ?? estimateTokens(args.content),
138
+ embedding: args.embedding,
139
+ tags: args.tags ?? [],
140
+ linkedContextEntryId: args.linkedContextEntryId,
141
+ consolidatedFrom: undefined,
142
+ createdAt: now,
143
+ updatedAt: now,
144
+ expiresAt: args.expiresAt,
145
+ });
146
+
147
+ return { memoryId, created: true };
148
+ },
149
+ });
150
+
151
+ export const searchMemory = query({
152
+ args: {
153
+ agentId: v.optional(v.string()),
154
+ tier: v.optional(memoryTierValidator),
155
+ queryText: v.optional(v.string()),
156
+ tags: v.optional(v.array(v.string())),
157
+ queryEmbedding: v.optional(v.array(v.number())),
158
+ limit: v.optional(v.number()),
159
+ },
160
+ handler: async (ctx, args) => {
161
+ let items = await ctx.db.query("agentMemory").collect();
162
+ if (args.agentId) items = items.filter((item) => item.agentId === args.agentId);
163
+ if (args.tier) items = items.filter((item) => item.tier === args.tier);
164
+ if (args.tags && args.tags.length > 0) {
165
+ const wanted = new Set(args.tags.map((tag) => tag.toLowerCase()));
166
+ items = items.filter((item) => item.tags.some((tag) => wanted.has(tag.toLowerCase())));
167
+ }
168
+
169
+ const scored = items.map((item) => {
170
+ const haystack = `${item.content}\n${item.summary ?? ""}\n${item.tags.join(" ")}`;
171
+ const lexical = args.queryText ? lexicalScore(haystack, args.queryText) : 0;
172
+ const vector =
173
+ args.queryEmbedding && Array.isArray(item.embedding)
174
+ ? cosineSimilarity(args.queryEmbedding, item.embedding)
175
+ : 0;
176
+ const recencyBoost = Math.max(0, 1 - (Date.now() - item.updatedAt) / (1000 * 60 * 60 * 24 * 30));
177
+ const score = lexical * 0.6 + vector * 0.3 + recencyBoost * 0.1;
178
+ return { item, score };
179
+ });
180
+
181
+ scored.sort((a, b) => b.score - a.score);
182
+ const limit = args.limit ?? 20;
183
+ return scored.slice(0, limit);
184
+ },
185
+ });
186
+
187
+ export const consolidateHotMemory = mutation({
188
+ args: {
189
+ agentId: v.string(),
190
+ maxTokens: v.optional(v.number()),
191
+ targetTier: v.optional(v.union(v.literal("warm"), v.literal("cold"))),
192
+ },
193
+ handler: async (ctx, { agentId, maxTokens, targetTier }) => {
194
+ const threshold = maxTokens ?? 40_000;
195
+ const target = targetTier ?? "warm";
196
+
197
+ const all = await ctx.db
198
+ .query("agentMemory")
199
+ .withIndex("by_agent_tier", (q: any) => q.eq("agentId", agentId).eq("tier", "hot"))
200
+ .collect();
201
+ const totalTokens = all.reduce((sum, item) => sum + item.tokens, 0);
202
+
203
+ if (totalTokens <= threshold || all.length === 0) {
204
+ return {
205
+ consolidated: false,
206
+ reason: "threshold_not_reached",
207
+ totalTokens,
208
+ threshold,
209
+ };
210
+ }
211
+
212
+ const ordered = [...all].sort((a, b) => a.updatedAt - b.updatedAt);
213
+ let rolling = 0;
214
+ const picked: any[] = [];
215
+ for (const item of ordered) {
216
+ picked.push(item);
217
+ rolling += item.tokens;
218
+ if (rolling >= threshold * 0.6) break;
219
+ }
220
+
221
+ const now = Date.now();
222
+ const consolidatedContent = picked
223
+ .map((item, index) => `(${index + 1}) ${item.summary ?? item.content}`)
224
+ .join("\n\n");
225
+
226
+ const consolidatedMemoryId = makeId("mem-consolidated");
227
+ await ctx.db.insert("agentMemory", {
228
+ memoryId: consolidatedMemoryId,
229
+ agentId,
230
+ taskId: undefined,
231
+ tier: target,
232
+ content: consolidatedContent,
233
+ summary: `Consolidated ${picked.length} hot memories`,
234
+ tokens: estimateTokens(consolidatedContent),
235
+ embedding: undefined,
236
+ tags: ["consolidated", "auto", target],
237
+ linkedContextEntryId: undefined,
238
+ consolidatedFrom: picked.map((item) => item.memoryId),
239
+ createdAt: now,
240
+ updatedAt: now,
241
+ expiresAt: undefined,
242
+ });
243
+
244
+ for (const item of picked) {
245
+ await ctx.db.patch(item._id, {
246
+ tier: "warm",
247
+ updatedAt: now,
248
+ });
249
+ }
250
+
251
+ const consolidationId = makeId("memc");
252
+ await ctx.db.insert("memoryConsolidations", {
253
+ consolidationId,
254
+ agentId,
255
+ sourceTier: "hot",
256
+ targetTier: target,
257
+ sourceMemoryIds: picked.map((item) => item.memoryId),
258
+ resultMemoryId: consolidatedMemoryId,
259
+ totalTokens: rolling,
260
+ createdAt: now,
261
+ });
262
+
263
+ return {
264
+ consolidated: true,
265
+ consolidationId,
266
+ sourceCount: picked.length,
267
+ sourceTokens: rolling,
268
+ resultMemoryId: consolidatedMemoryId,
269
+ targetTier: target,
270
+ };
271
+ },
272
+ });
273
+
274
+ export const syncMemoryToContext = mutation({
275
+ args: {
276
+ memoryId: v.string(),
277
+ projectId: v.optional(v.string()),
278
+ createdBy: v.optional(v.string()),
279
+ },
280
+ handler: async (ctx, { memoryId, projectId, createdBy }) => {
281
+ const memory = await ctx.db
282
+ .query("agentMemory")
283
+ .withIndex("by_memoryId", (q: any) => q.eq("memoryId", memoryId))
284
+ .first();
285
+ if (!memory) throw new Error(`Memory ${memoryId} not found`);
286
+
287
+ const now = Date.now();
288
+ const entryId = `ctx-${memory.memoryId}`;
289
+ const existing = await ctx.db
290
+ .query("contextEntries")
291
+ .filter((q: any) => q.eq(q.field("entryId"), entryId))
292
+ .first();
293
+
294
+ const base = {
295
+ projectId: projectId ?? "uca",
296
+ type: "learning",
297
+ content: memory.content,
298
+ summary: memory.summary,
299
+ tier: tierToContextTier(memory.tier),
300
+ promotionHistory: existing?.promotionHistory ?? [],
301
+ confidenceScore: memory.tier === "cold" ? 5 : memory.tier === "warm" ? 4 : 3,
302
+ validatedBy: existing?.validatedBy ?? [memory.agentId],
303
+ taskId: memory.taskId,
304
+ tags: [
305
+ ...new Set([...(memory.tags ?? []), "memory-sync", `tier:${memory.tier}`, `agent:${memory.agentId}`]),
306
+ ],
307
+ createdBy: createdBy ?? memory.agentId,
308
+ updatedAt: now,
309
+ };
310
+
311
+ if (existing) {
312
+ await ctx.db.patch(existing._id, base);
313
+ await ctx.db.patch(memory._id, { linkedContextEntryId: existing.entryId, updatedAt: now });
314
+ return { entryId: existing.entryId, updated: true };
315
+ }
316
+
317
+ await ctx.db.insert("contextEntries", {
318
+ entryId,
319
+ ...base,
320
+ createdAt: now,
321
+ });
322
+
323
+ await ctx.db.patch(memory._id, { linkedContextEntryId: entryId, updatedAt: now });
324
+ return { entryId, created: true };
325
+ },
326
+ });
@@ -0,0 +1,238 @@
1
+ import { internalMutation, mutation, query } from "../_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ const messageTypeValidator = v.union(
5
+ v.literal("handoff"),
6
+ v.literal("notification"),
7
+ v.literal("request"),
8
+ v.literal("response"),
9
+ v.literal("broadcast"),
10
+ );
11
+
12
+ const messageStatusValidator = v.union(
13
+ v.literal("pending"),
14
+ v.literal("delivered"),
15
+ v.literal("acknowledged"),
16
+ v.literal("expired"),
17
+ );
18
+
19
+ function makeId(prefix: string): string {
20
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
21
+ }
22
+
23
+ async function findMessage(ctx: any, messageId: string): Promise<any | null> {
24
+ return await ctx.db
25
+ .query("sessionBridgeMessages")
26
+ .withIndex("by_messageId", (q: any) => q.eq("messageId", messageId))
27
+ .first();
28
+ }
29
+
30
+ async function findTask(ctx: any, taskId: string): Promise<any | null> {
31
+ return await ctx.db
32
+ .query("tasks")
33
+ .withIndex("by_taskId", (q: any) => q.eq("taskId", taskId))
34
+ .first();
35
+ }
36
+
37
+ export const sendMessage = mutation({
38
+ args: {
39
+ messageId: v.optional(v.string()),
40
+ fromSessionId: v.string(),
41
+ toSessionId: v.optional(v.string()),
42
+ fromAgent: v.string(),
43
+ toAgent: v.optional(v.string()),
44
+ taskId: v.optional(v.string()),
45
+ messageType: messageTypeValidator,
46
+ content: v.string(),
47
+ summary: v.optional(v.string()),
48
+ expiresInMinutes: v.optional(v.number()),
49
+ },
50
+ handler: async (ctx, args) => {
51
+ const now = Date.now();
52
+ const messageId = args.messageId ?? makeId("msg");
53
+
54
+ const existing = await findMessage(ctx, messageId);
55
+ if (existing) return { messageId, duplicated: true };
56
+
57
+ const expiresAt =
58
+ typeof args.expiresInMinutes === "number" && args.expiresInMinutes > 0
59
+ ? now + args.expiresInMinutes * 60_000
60
+ : undefined;
61
+
62
+ await ctx.db.insert("sessionBridgeMessages", {
63
+ messageId,
64
+ fromSessionId: args.fromSessionId,
65
+ toSessionId: args.toSessionId,
66
+ fromAgent: args.fromAgent,
67
+ toAgent: args.toAgent,
68
+ taskId: args.taskId,
69
+ messageType: args.messageType,
70
+ content: args.content,
71
+ summary: args.summary,
72
+ status: "pending",
73
+ createdAt: now,
74
+ deliveredAt: undefined,
75
+ acknowledgedAt: undefined,
76
+ expiresAt,
77
+ });
78
+
79
+ return { messageId, status: "pending", expiresAt };
80
+ },
81
+ });
82
+
83
+ export const inbox = query({
84
+ args: {
85
+ toSessionId: v.optional(v.string()),
86
+ toAgent: v.optional(v.string()),
87
+ status: v.optional(messageStatusValidator),
88
+ includeBroadcast: v.optional(v.boolean()),
89
+ limit: v.optional(v.number()),
90
+ },
91
+ handler: async (ctx, args) => {
92
+ const all = await ctx.db.query("sessionBridgeMessages").collect();
93
+ let messages = all.filter((message) => {
94
+ if (args.status && message.status !== args.status) return false;
95
+
96
+ const addressedToSession = args.toSessionId ? message.toSessionId === args.toSessionId : false;
97
+ const addressedToAgent = args.toAgent ? message.toAgent === args.toAgent : false;
98
+ const isBroadcast = message.messageType === "broadcast";
99
+
100
+ if (args.includeBroadcast ?? true) {
101
+ return addressedToSession || addressedToAgent || isBroadcast;
102
+ }
103
+ return addressedToSession || addressedToAgent;
104
+ });
105
+
106
+ messages.sort((a, b) => b.createdAt - a.createdAt);
107
+ return args.limit ? messages.slice(0, args.limit) : messages;
108
+ },
109
+ });
110
+
111
+ export const markDelivered = mutation({
112
+ args: {
113
+ messageId: v.string(),
114
+ deliveredBySessionId: v.optional(v.string()),
115
+ },
116
+ handler: async (ctx, { messageId }) => {
117
+ const message = await findMessage(ctx, messageId);
118
+ if (!message) throw new Error(`Message ${messageId} not found`);
119
+ if (message.status !== "pending") return { messageId, status: message.status };
120
+
121
+ await ctx.db.patch(message._id, {
122
+ status: "delivered",
123
+ deliveredAt: Date.now(),
124
+ });
125
+
126
+ return { messageId, status: "delivered" };
127
+ },
128
+ });
129
+
130
+ export const acknowledgeMessage = mutation({
131
+ args: {
132
+ messageId: v.string(),
133
+ acknowledgedBy: v.optional(v.string()),
134
+ },
135
+ handler: async (ctx, { messageId }) => {
136
+ const message = await findMessage(ctx, messageId);
137
+ if (!message) throw new Error(`Message ${messageId} not found`);
138
+
139
+ await ctx.db.patch(message._id, {
140
+ status: "acknowledged",
141
+ acknowledgedAt: Date.now(),
142
+ deliveredAt: message.deliveredAt ?? Date.now(),
143
+ });
144
+
145
+ return { messageId, status: "acknowledged" };
146
+ },
147
+ });
148
+
149
+ export const handoffTask = mutation({
150
+ args: {
151
+ fromSessionId: v.string(),
152
+ toSessionId: v.string(),
153
+ fromAgent: v.string(),
154
+ toAgent: v.string(),
155
+ taskId: v.string(),
156
+ summary: v.string(),
157
+ content: v.optional(v.string()),
158
+ contextRefs: v.optional(v.array(v.string())),
159
+ },
160
+ handler: async (ctx, args) => {
161
+ const task = await findTask(ctx, args.taskId);
162
+ if (!task) throw new Error(`Task ${args.taskId} not found`);
163
+
164
+ const messageId = makeId("handoff");
165
+ const now = Date.now();
166
+ const contextBlock = (args.contextRefs ?? []).length
167
+ ? `\n\nContext refs:\n- ${(args.contextRefs ?? []).join("\n- ")}`
168
+ : "";
169
+
170
+ await ctx.db.insert("sessionBridgeMessages", {
171
+ messageId,
172
+ fromSessionId: args.fromSessionId,
173
+ toSessionId: args.toSessionId,
174
+ fromAgent: args.fromAgent,
175
+ toAgent: args.toAgent,
176
+ taskId: args.taskId,
177
+ messageType: "handoff",
178
+ content: `${args.content ?? args.summary}${contextBlock}`,
179
+ summary: args.summary,
180
+ status: "pending",
181
+ createdAt: now,
182
+ deliveredAt: undefined,
183
+ acknowledgedAt: undefined,
184
+ expiresAt: undefined,
185
+ });
186
+
187
+ const history = Array.isArray(task.statusHistory) ? task.statusHistory : [];
188
+ await ctx.db.patch(task._id, {
189
+ agent: args.toAgent,
190
+ status: task.status === "done" ? "review" : "todo",
191
+ updatedAt: now,
192
+ statusHistory: [
193
+ ...history,
194
+ {
195
+ status: task.status === "done" ? "review" : "todo",
196
+ timestamp: now,
197
+ agent: args.fromAgent,
198
+ note: `Session handoff -> ${args.toAgent} (${args.toSessionId})`,
199
+ },
200
+ ],
201
+ notes: [task.notes ?? "", `\n\nHandoff summary (${new Date(now).toISOString()}):\n${args.summary}`].join(""),
202
+ });
203
+
204
+ return {
205
+ messageId,
206
+ taskId: args.taskId,
207
+ nextAgent: args.toAgent,
208
+ nextSessionId: args.toSessionId,
209
+ };
210
+ },
211
+ });
212
+
213
+ export const internalExpireMessages = internalMutation({
214
+ args: {},
215
+ handler: async (ctx) => {
216
+ const now = Date.now();
217
+ const all = await ctx.db.query("sessionBridgeMessages").collect();
218
+ const expired = all.filter(
219
+ (message) =>
220
+ typeof message.expiresAt === "number" &&
221
+ message.expiresAt <= now &&
222
+ message.status !== "acknowledged" &&
223
+ message.status !== "expired",
224
+ );
225
+
226
+ for (const message of expired) {
227
+ await ctx.db.patch(message._id, {
228
+ status: "expired",
229
+ });
230
+ }
231
+
232
+ return {
233
+ scanned: all.length,
234
+ expired: expired.length,
235
+ messageIds: expired.map((message) => message.messageId),
236
+ };
237
+ },
238
+ });