@spinabot/brigade 1.0.1 → 1.0.2

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 (51) hide show
  1. package/convex/_generated/api.d.ts +85 -0
  2. package/convex/_generated/api.js +23 -0
  3. package/convex/_generated/dataModel.d.ts +60 -0
  4. package/convex/_generated/server.d.ts +143 -0
  5. package/convex/_generated/server.js +93 -0
  6. package/convex/admin.d.ts +57 -0
  7. package/convex/admin.ts +315 -0
  8. package/convex/auth.d.ts +159 -0
  9. package/convex/auth.ts +217 -0
  10. package/convex/blobs.d.ts +38 -0
  11. package/convex/blobs.ts +115 -0
  12. package/convex/channels.d.ts +150 -0
  13. package/convex/channels.ts +455 -0
  14. package/convex/config.d.ts +67 -0
  15. package/convex/config.ts +168 -0
  16. package/convex/cron.d.ts +237 -0
  17. package/convex/cron.ts +199 -0
  18. package/convex/execApprovals.d.ts +31 -0
  19. package/convex/execApprovals.ts +58 -0
  20. package/convex/extensions.d.ts +30 -0
  21. package/convex/extensions.ts +51 -0
  22. package/convex/health.d.ts +18 -0
  23. package/convex/health.ts +69 -0
  24. package/convex/instance.d.ts +34 -0
  25. package/convex/instance.ts +82 -0
  26. package/convex/logs.d.ts +178 -0
  27. package/convex/logs.ts +253 -0
  28. package/convex/memory.d.ts +354 -0
  29. package/convex/memory.ts +536 -0
  30. package/convex/messages.d.ts +124 -0
  31. package/convex/messages.ts +347 -0
  32. package/convex/org.d.ts +75 -0
  33. package/convex/org.ts +99 -0
  34. package/convex/schema.d.ts +1130 -0
  35. package/convex/schema.ts +847 -0
  36. package/convex/sessions.d.ts +100 -0
  37. package/convex/sessions.ts +105 -0
  38. package/convex/skills.d.ts +73 -0
  39. package/convex/skills.ts +102 -0
  40. package/convex/subagents.d.ts +214 -0
  41. package/convex/subagents.ts +99 -0
  42. package/convex/tsconfig.json +23 -0
  43. package/convex/whatsappAuth.d.ts +52 -0
  44. package/convex/whatsappAuth.ts +151 -0
  45. package/convex/workspace.d.ts +49 -0
  46. package/convex/workspace.ts +106 -0
  47. package/dist/buildstamp.json +1 -1
  48. package/package.json +7 -1
  49. package/scripts/convex-dev.mjs +321 -0
  50. package/scripts/convex-push.mjs +69 -0
  51. package/scripts/install-convex.mjs +123 -0
@@ -0,0 +1,34 @@
1
+ export declare const getCoord: import("convex/server").RegisteredQuery<"public", {
2
+ instanceId: string;
3
+ }, Promise<{
4
+ _id: import("convex/values").GenericId<"gatewayCoord">;
5
+ _creationTime: number;
6
+ pid?: number | undefined;
7
+ pidAliveAt?: number | undefined;
8
+ heartbeatTs?: number | undefined;
9
+ heartbeatPid?: number | undefined;
10
+ heartbeatUptimeMs?: number | undefined;
11
+ lockPid?: number | undefined;
12
+ lockPort?: number | undefined;
13
+ lockCreatedAt?: string | undefined;
14
+ lockLeaseUntil?: number | undefined;
15
+ instanceId: string;
16
+ updatedAt: number;
17
+ } | null>>;
18
+ export declare const writePid: import("convex/server").RegisteredMutation<"public", {
19
+ pid: number;
20
+ instanceId: string;
21
+ }, Promise<void>>;
22
+ export declare const clearPid: import("convex/server").RegisteredMutation<"public", {
23
+ instanceId: string;
24
+ }, Promise<void>>;
25
+ export declare const writeHeartbeat: import("convex/server").RegisteredMutation<"public", {
26
+ ts: number;
27
+ pid: number;
28
+ uptimeMs: number;
29
+ instanceId: string;
30
+ }, Promise<void>>;
31
+ export declare const clearHeartbeat: import("convex/server").RegisteredMutation<"public", {
32
+ instanceId: string;
33
+ }, Promise<void>>;
34
+ //# sourceMappingURL=instance.d.ts.map
@@ -0,0 +1,82 @@
1
+ // convex/instance.ts — gatewayCoord (heartbeat/pid; the lock stays LOCAL — fs.open("wx") has no Convex equivalent)
2
+ import { v } from "convex/values";
3
+ import { mutation, query } from "./_generated/server.js";
4
+
5
+ export const getCoord = query({
6
+ args: { instanceId: v.string() },
7
+ handler: async (ctx, args) => {
8
+ return ctx.db
9
+ .query("gatewayCoord")
10
+ .withIndex("by_instance", (q) => q.eq("instanceId", args.instanceId))
11
+ .first();
12
+ },
13
+ });
14
+
15
+ export const writePid = mutation({
16
+ args: { instanceId: v.string(), pid: v.number() },
17
+ handler: async (ctx, args) => {
18
+ const existing = await ctx.db
19
+ .query("gatewayCoord")
20
+ .withIndex("by_instance", (q) => q.eq("instanceId", args.instanceId))
21
+ .first();
22
+ const now = Date.now();
23
+ const payload = {
24
+ instanceId: args.instanceId,
25
+ pid: args.pid,
26
+ pidAliveAt: now,
27
+ updatedAt: now,
28
+ };
29
+ if (existing) await ctx.db.patch(existing._id, payload);
30
+ else await ctx.db.insert("gatewayCoord", payload);
31
+ },
32
+ });
33
+
34
+ export const clearPid = mutation({
35
+ args: { instanceId: v.string() },
36
+ handler: async (ctx, args) => {
37
+ const existing = await ctx.db
38
+ .query("gatewayCoord")
39
+ .withIndex("by_instance", (q) => q.eq("instanceId", args.instanceId))
40
+ .first();
41
+ if (existing) {
42
+ await ctx.db.patch(existing._id, { pid: undefined, pidAliveAt: undefined, updatedAt: Date.now() });
43
+ }
44
+ },
45
+ });
46
+
47
+ export const writeHeartbeat = mutation({
48
+ args: { instanceId: v.string(), ts: v.number(), pid: v.number(), uptimeMs: v.number() },
49
+ handler: async (ctx, args) => {
50
+ const existing = await ctx.db
51
+ .query("gatewayCoord")
52
+ .withIndex("by_instance", (q) => q.eq("instanceId", args.instanceId))
53
+ .first();
54
+ const payload = {
55
+ instanceId: args.instanceId,
56
+ heartbeatTs: args.ts,
57
+ heartbeatPid: args.pid,
58
+ heartbeatUptimeMs: args.uptimeMs,
59
+ updatedAt: Date.now(),
60
+ };
61
+ if (existing) await ctx.db.patch(existing._id, payload);
62
+ else await ctx.db.insert("gatewayCoord", payload);
63
+ },
64
+ });
65
+
66
+ export const clearHeartbeat = mutation({
67
+ args: { instanceId: v.string() },
68
+ handler: async (ctx, args) => {
69
+ const existing = await ctx.db
70
+ .query("gatewayCoord")
71
+ .withIndex("by_instance", (q) => q.eq("instanceId", args.instanceId))
72
+ .first();
73
+ if (existing) {
74
+ await ctx.db.patch(existing._id, {
75
+ heartbeatTs: undefined,
76
+ heartbeatPid: undefined,
77
+ heartbeatUptimeMs: undefined,
78
+ updatedAt: Date.now(),
79
+ });
80
+ }
81
+ },
82
+ });
@@ -0,0 +1,178 @@
1
+ export declare const appendSessionEvent: import("convex/server").RegisteredMutation<"public", {
2
+ toolName?: string | undefined;
3
+ content?: ArrayBuffer | undefined;
4
+ args?: ArrayBuffer | undefined;
5
+ aborted?: boolean | undefined;
6
+ inner?: string | undefined;
7
+ delta?: string | undefined;
8
+ role?: string | undefined;
9
+ stopReason?: string | undefined;
10
+ errorMessage?: string | undefined;
11
+ toolCallId?: string | undefined;
12
+ isError?: boolean | undefined;
13
+ result?: ArrayBuffer | undefined;
14
+ attempt?: number | undefined;
15
+ maxAttempts?: number | undefined;
16
+ delayMs?: number | undefined;
17
+ success?: boolean | undefined;
18
+ finalError?: string | undefined;
19
+ willRetry?: boolean | undefined;
20
+ messageCount?: number | undefined;
21
+ type: string;
22
+ agentId: string;
23
+ sessionKey: string;
24
+ ts: string;
25
+ day: string;
26
+ ownerId: string;
27
+ }, Promise<void>>;
28
+ export declare const readSessionEventTail: import("convex/server").RegisteredQuery<"public", {
29
+ limit?: number | undefined;
30
+ day?: string | undefined;
31
+ ownerId: string;
32
+ }, Promise<{
33
+ _id: import("convex/values").GenericId<"sessionEvents">;
34
+ _creationTime: number;
35
+ toolName?: string | undefined;
36
+ content?: ArrayBuffer | undefined;
37
+ args?: ArrayBuffer | undefined;
38
+ aborted?: boolean | undefined;
39
+ inner?: string | undefined;
40
+ delta?: string | undefined;
41
+ role?: string | undefined;
42
+ stopReason?: string | undefined;
43
+ errorMessage?: string | undefined;
44
+ toolCallId?: string | undefined;
45
+ isError?: boolean | undefined;
46
+ result?: ArrayBuffer | undefined;
47
+ attempt?: number | undefined;
48
+ maxAttempts?: number | undefined;
49
+ delayMs?: number | undefined;
50
+ success?: boolean | undefined;
51
+ finalError?: string | undefined;
52
+ willRetry?: boolean | undefined;
53
+ messageCount?: number | undefined;
54
+ type: string;
55
+ agentId: string;
56
+ sessionKey: string;
57
+ ts: string;
58
+ day: string;
59
+ ownerId: string;
60
+ }[]>>;
61
+ export declare const findLastError: import("convex/server").RegisteredQuery<"public", {
62
+ limit?: number | undefined;
63
+ ownerId: string;
64
+ }, Promise<{
65
+ _id: import("convex/values").GenericId<"sessionEvents">;
66
+ _creationTime: number;
67
+ toolName?: string | undefined;
68
+ content?: ArrayBuffer | undefined;
69
+ args?: ArrayBuffer | undefined;
70
+ aborted?: boolean | undefined;
71
+ inner?: string | undefined;
72
+ delta?: string | undefined;
73
+ role?: string | undefined;
74
+ stopReason?: string | undefined;
75
+ errorMessage?: string | undefined;
76
+ toolCallId?: string | undefined;
77
+ isError?: boolean | undefined;
78
+ result?: ArrayBuffer | undefined;
79
+ attempt?: number | undefined;
80
+ maxAttempts?: number | undefined;
81
+ delayMs?: number | undefined;
82
+ success?: boolean | undefined;
83
+ finalError?: string | undefined;
84
+ willRetry?: boolean | undefined;
85
+ messageCount?: number | undefined;
86
+ type: string;
87
+ agentId: string;
88
+ sessionKey: string;
89
+ ts: string;
90
+ day: string;
91
+ ownerId: string;
92
+ } | null>>;
93
+ export declare const appendSubsystemRecord: import("convex/server").RegisteredMutation<"public", {
94
+ fields?: any;
95
+ message: string;
96
+ time: string;
97
+ level: "error" | "trace" | "debug" | "info" | "warn" | "fatal";
98
+ subsystem: string;
99
+ day: string;
100
+ ownerId: string;
101
+ }, Promise<void>>;
102
+ export declare const readSubsystemRecords: import("convex/server").RegisteredQuery<"public", {
103
+ level?: "error" | "trace" | "debug" | "info" | "warn" | "fatal" | undefined;
104
+ subsystem?: string | undefined;
105
+ limit?: number | undefined;
106
+ day?: string | undefined;
107
+ ownerId: string;
108
+ }, Promise<{
109
+ _id: import("convex/values").GenericId<"subsystemLog">;
110
+ _creationTime: number;
111
+ fields?: any;
112
+ message: string;
113
+ time: string;
114
+ level: string;
115
+ subsystem: string;
116
+ day: string;
117
+ ownerId: string;
118
+ }[]>>;
119
+ export declare const pruneSubsystemLogs: import("convex/server").RegisteredMutation<"public", {
120
+ ownerId: string;
121
+ olderThanMs: number;
122
+ }, Promise<{
123
+ removed: number;
124
+ }>>;
125
+ export declare const appendConfigAudit: import("convex/server").RegisteredMutation<"public", {
126
+ pid?: number | undefined;
127
+ sha256: string;
128
+ ts: string;
129
+ bytes: number;
130
+ instanceId: string;
131
+ }, Promise<{
132
+ pid?: number | undefined;
133
+ prevHash?: string | undefined;
134
+ instanceId: string;
135
+ ts: string;
136
+ sha256: string;
137
+ bytes: number;
138
+ seq: number;
139
+ lineHash: string;
140
+ }>>;
141
+ export declare const listConfigAudit: import("convex/server").RegisteredQuery<"public", {
142
+ limit?: number | undefined;
143
+ instanceId: string;
144
+ }, Promise<{
145
+ _id: import("convex/values").GenericId<"brigadeConfigAudit">;
146
+ _creationTime: number;
147
+ pid?: number | undefined;
148
+ prevHash?: string | undefined;
149
+ sha256: string;
150
+ ts: string;
151
+ bytes: number;
152
+ instanceId: string;
153
+ lineHash: string;
154
+ seq: number;
155
+ }[]>>;
156
+ export declare const writeConfigHealth: import("convex/server").RegisteredMutation<"public", {
157
+ sha256: string;
158
+ mtimeMs: number;
159
+ ts: string;
160
+ pid: number;
161
+ bytes: number;
162
+ ownerId: string;
163
+ configPath: string;
164
+ }, Promise<void>>;
165
+ export declare const readConfigHealth: import("convex/server").RegisteredQuery<"public", {
166
+ ownerId: string;
167
+ }, Promise<{
168
+ _id: import("convex/values").GenericId<"configHealth">;
169
+ _creationTime: number;
170
+ sha256: string;
171
+ mtimeMs: number;
172
+ ts: string;
173
+ pid: number;
174
+ bytes: number;
175
+ ownerId: string;
176
+ configPath: string;
177
+ } | null>>;
178
+ //# sourceMappingURL=logs.d.ts.map
package/convex/logs.ts ADDED
@@ -0,0 +1,253 @@
1
+ // convex/logs.ts — sessionEvents + subsystemLog + brigadeConfigAudit + configHealth
2
+ import { v } from "convex/values";
3
+ import { mutation, query } from "./_generated/server.js";
4
+
5
+ const Level = v.union(
6
+ v.literal("trace"),
7
+ v.literal("debug"),
8
+ v.literal("info"),
9
+ v.literal("warn"),
10
+ v.literal("error"),
11
+ v.literal("fatal"),
12
+ );
13
+
14
+ // ============================================================================
15
+ // sessionEvents (Pi session events)
16
+ // ============================================================================
17
+
18
+ export const appendSessionEvent = mutation({
19
+ args: {
20
+ ts: v.string(),
21
+ day: v.string(),
22
+ ownerId: v.string(),
23
+ agentId: v.string(),
24
+ sessionKey: v.string(),
25
+ type: v.string(),
26
+ inner: v.optional(v.string()),
27
+ delta: v.optional(v.string()),
28
+ toolCallId: v.optional(v.string()),
29
+ toolName: v.optional(v.string()),
30
+ args: v.optional(v.bytes()),
31
+ result: v.optional(v.bytes()),
32
+ isError: v.optional(v.boolean()),
33
+ role: v.optional(v.string()),
34
+ content: v.optional(v.bytes()),
35
+ stopReason: v.optional(v.string()),
36
+ errorMessage: v.optional(v.string()),
37
+ attempt: v.optional(v.number()),
38
+ maxAttempts: v.optional(v.number()),
39
+ delayMs: v.optional(v.number()),
40
+ aborted: v.optional(v.boolean()),
41
+ willRetry: v.optional(v.boolean()),
42
+ messageCount: v.optional(v.number()),
43
+ success: v.optional(v.boolean()),
44
+ finalError: v.optional(v.string()),
45
+ },
46
+ handler: async (ctx, args) => {
47
+ await ctx.db.insert("sessionEvents", args);
48
+ },
49
+ });
50
+
51
+ export const readSessionEventTail = query({
52
+ args: { ownerId: v.string(), day: v.optional(v.string()), limit: v.optional(v.number()) },
53
+ handler: async (ctx, args) => {
54
+ const day = args.day ?? new Date().toISOString().slice(0, 10);
55
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
56
+ // Take the most-recent `limit` (desc) but RETURN them chronological
57
+ // (oldest-first) to match the disk reader (event-logger.readSession-
58
+ // EventTail walks the file tail in append order). A consumer replaying
59
+ // the tail (e.g. convex→filesystem migrate) would otherwise reverse it.
60
+ const rows = await ctx.db
61
+ .query("sessionEvents")
62
+ .withIndex("by_owner_day", (q) => q.eq("ownerId", args.ownerId).eq("day", day))
63
+ .order("desc")
64
+ .take(limit);
65
+ return rows.reverse();
66
+ },
67
+ });
68
+
69
+ export const findLastError = query({
70
+ args: { ownerId: v.string(), limit: v.optional(v.number()) },
71
+ handler: async (ctx, args) => {
72
+ // Mirror the disk `getLastLoggedError` scan: the most recent error is not
73
+ // always a tool error (isError) — it can be an auto_retry_start with an
74
+ // errorMessage or an aborted compaction_end. Scan today's events
75
+ // newest-first and return the first error-shaped row. Falls back to the
76
+ // isError index for older days when today has none.
77
+ const day = new Date().toISOString().slice(0, 10);
78
+ const scan = args.limit && args.limit > 0 ? args.limit : 400;
79
+ const recent = await ctx.db
80
+ .query("sessionEvents")
81
+ .withIndex("by_owner_day", (q) => q.eq("ownerId", args.ownerId).eq("day", day))
82
+ .order("desc")
83
+ .take(scan);
84
+ for (const row of recent) {
85
+ if (row.type === "tool_execution_end" && row.isError === true) return row;
86
+ if (row.type === "auto_retry_start" && typeof row.errorMessage === "string") return row;
87
+ if (row.type === "compaction_end" && row.aborted === true && typeof row.errorMessage === "string") {
88
+ return row;
89
+ }
90
+ }
91
+ // Nothing today — fall back to the newest tool error across all days.
92
+ return ctx.db
93
+ .query("sessionEvents")
94
+ .withIndex("by_owner_error", (q) => q.eq("ownerId", args.ownerId).eq("isError", true))
95
+ .order("desc")
96
+ .first();
97
+ },
98
+ });
99
+
100
+ // ============================================================================
101
+ // subsystemLog
102
+ // ============================================================================
103
+
104
+ export const appendSubsystemRecord = mutation({
105
+ args: {
106
+ time: v.string(),
107
+ day: v.string(),
108
+ ownerId: v.string(),
109
+ level: Level,
110
+ subsystem: v.string(),
111
+ message: v.string(),
112
+ fields: v.optional(v.any()),
113
+ },
114
+ handler: async (ctx, args) => {
115
+ await ctx.db.insert("subsystemLog", args);
116
+ },
117
+ });
118
+
119
+ export const readSubsystemRecords = query({
120
+ args: {
121
+ ownerId: v.string(),
122
+ day: v.optional(v.string()),
123
+ level: v.optional(Level),
124
+ subsystem: v.optional(v.string()),
125
+ limit: v.optional(v.number()),
126
+ },
127
+ handler: async (ctx, args) => {
128
+ const limit = args.limit && args.limit > 0 ? args.limit : 200;
129
+ const day = args.day ?? new Date().toISOString().slice(0, 10);
130
+ let rows = await ctx.db
131
+ .query("subsystemLog")
132
+ .withIndex("by_owner_day", (q) => q.eq("ownerId", args.ownerId).eq("day", day))
133
+ .order("desc")
134
+ .take(limit * 4);
135
+ if (args.level) rows = rows.filter((r) => r.level === args.level);
136
+ if (args.subsystem) rows = rows.filter((r) => r.subsystem === args.subsystem);
137
+ return rows.slice(0, limit);
138
+ },
139
+ });
140
+
141
+ export const pruneSubsystemLogs = mutation({
142
+ args: { ownerId: v.string(), olderThanMs: v.number() },
143
+ handler: async (ctx, args) => {
144
+ const cutoff = new Date(Date.now() - args.olderThanMs).toISOString();
145
+ // Prune ONE bounded page of expired rows; the client loops until
146
+ // {removed:0}. `.collect()` of the whole (unbounded, high-volume)
147
+ // subsystemLog blows the 16 MiB read + 8k-delete caps once it's large —
148
+ // which is exactly when prune runs. Expired rows sort FIRST on
149
+ // by_owner_day (oldest day first), so each page makes progress until
150
+ // only kept rows remain. Page size is small + count-bounded (log rows
151
+ // are tiny) to stay under the read cap. Lossless.
152
+ const rows = await ctx.db
153
+ .query("subsystemLog")
154
+ .withIndex("by_owner_day", (q) => q.eq("ownerId", args.ownerId))
155
+ .order("asc")
156
+ .take(500);
157
+ let removed = 0;
158
+ for (const r of rows) {
159
+ if (r.time < cutoff) {
160
+ await ctx.db.delete(r._id);
161
+ removed += 1;
162
+ }
163
+ }
164
+ return { removed };
165
+ },
166
+ });
167
+
168
+ // ============================================================================
169
+ // brigadeConfigAudit (hash-chained)
170
+ // ============================================================================
171
+
172
+ export const appendConfigAudit = mutation({
173
+ args: {
174
+ instanceId: v.string(),
175
+ ts: v.string(),
176
+ sha256: v.string(),
177
+ bytes: v.number(),
178
+ pid: v.optional(v.number()),
179
+ },
180
+ handler: async (ctx, args) => {
181
+ const tail = await ctx.db
182
+ .query("brigadeConfigAudit")
183
+ .withIndex("by_instance_seq", (q) => q.eq("instanceId", args.instanceId))
184
+ .order("desc")
185
+ .first();
186
+ const seq = (tail?.seq ?? 0) + 1;
187
+ const prevHash = tail?.lineHash;
188
+ const lineHashInput = `${args.instanceId}|${args.ts}|${args.sha256}|${args.bytes}|${seq}|${prevHash ?? ""}`;
189
+ const enc = new TextEncoder().encode(lineHashInput);
190
+ const buf = await crypto.subtle.digest("SHA-256", enc);
191
+ const lineHash = Array.from(new Uint8Array(buf))
192
+ .map((b) => b.toString(16).padStart(2, "0"))
193
+ .join("");
194
+ const payload = {
195
+ instanceId: args.instanceId,
196
+ ts: args.ts,
197
+ sha256: args.sha256,
198
+ bytes: args.bytes,
199
+ seq,
200
+ lineHash,
201
+ ...(prevHash !== undefined ? { prevHash } : {}),
202
+ ...(args.pid !== undefined ? { pid: args.pid } : {}),
203
+ };
204
+ await ctx.db.insert("brigadeConfigAudit", payload);
205
+ return payload;
206
+ },
207
+ });
208
+
209
+ export const listConfigAudit = query({
210
+ args: { instanceId: v.string(), limit: v.optional(v.number()) },
211
+ handler: async (ctx, args) => {
212
+ const limit = args.limit && args.limit > 0 ? args.limit : 100;
213
+ return ctx.db
214
+ .query("brigadeConfigAudit")
215
+ .withIndex("by_instance_seq", (q) => q.eq("instanceId", args.instanceId))
216
+ .order("asc")
217
+ .take(limit);
218
+ },
219
+ });
220
+
221
+ // ============================================================================
222
+ // configHealth
223
+ // ============================================================================
224
+
225
+ export const writeConfigHealth = mutation({
226
+ args: {
227
+ ownerId: v.string(),
228
+ ts: v.string(),
229
+ configPath: v.string(),
230
+ bytes: v.number(),
231
+ sha256: v.string(),
232
+ mtimeMs: v.number(),
233
+ pid: v.number(),
234
+ },
235
+ handler: async (ctx, args) => {
236
+ const existing = await ctx.db
237
+ .query("configHealth")
238
+ .withIndex("by_owner", (q) => q.eq("ownerId", args.ownerId))
239
+ .first();
240
+ if (existing) await ctx.db.replace(existing._id, args);
241
+ else await ctx.db.insert("configHealth", args);
242
+ },
243
+ });
244
+
245
+ export const readConfigHealth = query({
246
+ args: { ownerId: v.string() },
247
+ handler: async (ctx, args) => {
248
+ return ctx.db
249
+ .query("configHealth")
250
+ .withIndex("by_owner", (q) => q.eq("ownerId", args.ownerId))
251
+ .first();
252
+ },
253
+ });