@agentuity/opencode 1.0.11 → 1.0.13

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 (48) hide show
  1. package/dist/agents/lead.d.ts +1 -1
  2. package/dist/agents/lead.d.ts.map +1 -1
  3. package/dist/agents/lead.js +9 -0
  4. package/dist/agents/lead.js.map +1 -1
  5. package/dist/agents/monitor.d.ts +1 -1
  6. package/dist/agents/monitor.d.ts.map +1 -1
  7. package/dist/agents/monitor.js +13 -0
  8. package/dist/agents/monitor.js.map +1 -1
  9. package/dist/background/manager.d.ts +4 -1
  10. package/dist/background/manager.d.ts.map +1 -1
  11. package/dist/background/manager.js +161 -3
  12. package/dist/background/manager.js.map +1 -1
  13. package/dist/background/types.d.ts +21 -0
  14. package/dist/background/types.d.ts.map +1 -1
  15. package/dist/plugin/hooks/cadence.d.ts +2 -1
  16. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  17. package/dist/plugin/hooks/cadence.js +57 -1
  18. package/dist/plugin/hooks/cadence.js.map +1 -1
  19. package/dist/plugin/plugin.d.ts.map +1 -1
  20. package/dist/plugin/plugin.js +196 -7
  21. package/dist/plugin/plugin.js.map +1 -1
  22. package/dist/sqlite/index.d.ts +3 -0
  23. package/dist/sqlite/index.d.ts.map +1 -0
  24. package/dist/sqlite/index.js +2 -0
  25. package/dist/sqlite/index.js.map +1 -0
  26. package/dist/sqlite/queries.d.ts +18 -0
  27. package/dist/sqlite/queries.d.ts.map +1 -0
  28. package/dist/sqlite/queries.js +41 -0
  29. package/dist/sqlite/queries.js.map +1 -0
  30. package/dist/sqlite/reader.d.ts +44 -0
  31. package/dist/sqlite/reader.d.ts.map +1 -0
  32. package/dist/sqlite/reader.js +526 -0
  33. package/dist/sqlite/reader.js.map +1 -0
  34. package/dist/sqlite/types.d.ts +110 -0
  35. package/dist/sqlite/types.d.ts.map +1 -0
  36. package/dist/sqlite/types.js +2 -0
  37. package/dist/sqlite/types.js.map +1 -0
  38. package/package.json +3 -3
  39. package/src/agents/lead.ts +9 -0
  40. package/src/agents/monitor.ts +13 -0
  41. package/src/background/manager.ts +174 -3
  42. package/src/background/types.ts +10 -0
  43. package/src/plugin/hooks/cadence.ts +72 -1
  44. package/src/plugin/plugin.ts +271 -23
  45. package/src/sqlite/index.ts +16 -0
  46. package/src/sqlite/queries.ts +50 -0
  47. package/src/sqlite/reader.ts +677 -0
  48. package/src/sqlite/types.ts +121 -0
@@ -1,6 +1,9 @@
1
1
  import type { PluginInput, Hooks } from '@opencode-ai/plugin';
2
2
  import { tool } from '@opencode-ai/plugin';
3
3
  import { StructuredError } from '@agentuity/core';
4
+ import { existsSync } from 'node:fs';
5
+ import { homedir, platform } from 'node:os';
6
+ import { join } from 'node:path';
4
7
  import { z } from 'zod';
5
8
  import type { AgentConfig, CommandDefinition } from '../types';
6
9
  import { loadAllSkills, type LoadedSkill } from '../skills';
@@ -14,6 +17,8 @@ import { createCadenceHooks } from './hooks/cadence';
14
17
  import { createSessionMemoryHooks } from './hooks/session-memory';
15
18
  import type { AgentRole } from '../types';
16
19
  import { BackgroundManager } from '../background';
20
+ import type { SessionTreeNode } from '../sqlite';
21
+ import { OpenCodeDBReader } from '../sqlite';
17
22
  import { TmuxSessionManager } from '../tmux';
18
23
  import { checkAuth } from '../services/auth';
19
24
 
@@ -40,6 +45,11 @@ const MemoryShareError = StructuredError(
40
45
  'Failed to create public memory share'
41
46
  )<{ reason: string }>();
42
47
 
48
+ const OpenCodeDashboardUnavailableError = StructuredError(
49
+ 'OpenCodeDashboardUnavailableError',
50
+ 'OpenCode SQLite database is not available. Requires OpenCode v1.2.0+ with SQLite storage.'
51
+ );
52
+
43
53
  // Sandbox environment detection
44
54
  const SANDBOX_ID = process.env.AGENTUITY_SANDBOX_ID;
45
55
  const IN_SANDBOX = !!SANDBOX_ID;
@@ -79,6 +89,8 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
79
89
 
80
90
  const userConfig = await loadCoderConfig();
81
91
  const coderConfig = mergeConfig(getDefaultConfig(), userConfig);
92
+ const resolvedDbPath = resolveOpenCodeDBPath();
93
+ const dbReader = new OpenCodeDBReader(resolvedDbPath ? { dbPath: resolvedDbPath } : undefined);
82
94
 
83
95
  const sessionHooks = createSessionHooks(ctx, coderConfig);
84
96
  const toolHooks = createToolHooks(ctx, coderConfig);
@@ -96,23 +108,28 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
96
108
  }),
97
109
  })
98
110
  : undefined;
99
- const backgroundManager = new BackgroundManager(ctx, coderConfig.background, {
100
- onSubagentSessionCreated: tmuxManager
101
- ? (event) => {
102
- void tmuxManager.onSessionCreated(event);
103
- }
104
- : undefined,
105
- onSubagentSessionDeleted: tmuxManager
106
- ? (event) => {
107
- void tmuxManager.onSessionDeleted(event);
108
- }
109
- : undefined,
110
- onShutdown: tmuxManager
111
- ? () => {
112
- void tmuxManager.cleanup();
113
- }
114
- : undefined,
115
- });
111
+ const backgroundManager = new BackgroundManager(
112
+ ctx,
113
+ coderConfig.background,
114
+ {
115
+ onSubagentSessionCreated: tmuxManager
116
+ ? (event) => {
117
+ void tmuxManager.onSessionCreated(event);
118
+ }
119
+ : undefined,
120
+ onSubagentSessionDeleted: tmuxManager
121
+ ? (event) => {
122
+ void tmuxManager.onSessionDeleted(event);
123
+ }
124
+ : undefined,
125
+ onShutdown: tmuxManager
126
+ ? () => {
127
+ void tmuxManager.cleanup();
128
+ }
129
+ : undefined,
130
+ },
131
+ dbReader
132
+ );
116
133
 
117
134
  // Recover any background tasks from previous sessions
118
135
  // This allows tasks to survive plugin restarts
@@ -140,7 +157,7 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
140
157
  });
141
158
 
142
159
  // Create hooks that need backgroundManager for task reference injection during compaction
143
- const cadenceHooks = createCadenceHooks(ctx, coderConfig, backgroundManager);
160
+ const cadenceHooks = createCadenceHooks(ctx, coderConfig, backgroundManager, dbReader);
144
161
 
145
162
  // Session memory hooks handle checkpointing and compaction for non-Cadence sessions
146
163
  // Orchestration (deciding which module handles which session) happens below in the hooks
@@ -149,7 +166,7 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
149
166
  const configHandler = createConfigHandler(coderConfig);
150
167
 
151
168
  // Create plugin tools using the @opencode-ai/plugin tool helper
152
- const tools = createTools(backgroundManager);
169
+ const tools = createTools(backgroundManager, dbReader);
153
170
 
154
171
  // Create a logger for shutdown handler
155
172
  const shutdownLogger = (message: string) =>
@@ -161,7 +178,7 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
161
178
  },
162
179
  });
163
180
 
164
- registerShutdownHandler(backgroundManager, tmuxManager, shutdownLogger);
181
+ registerShutdownHandler(backgroundManager, tmuxManager, shutdownLogger, dbReader);
165
182
 
166
183
  // Show startup toast (fire and forget, don't block)
167
184
  try {
@@ -192,6 +209,9 @@ export async function createCoderPlugin(ctx: PluginInput): Promise<Hooks> {
192
209
  out.env ??= {} as Record<string, string>;
193
210
  out.env.AGENTUITY_PROFILE = profile;
194
211
  out.env.AGENTUITY_AGENT_MODE = 'opencode';
212
+ if (resolvedDbPath) {
213
+ out.env.OPENCODE_DB_PATH = resolvedDbPath;
214
+ }
195
215
  const sessionId = process.env.AGENTUITY_OPENCODE_SESSION;
196
216
  if (sessionId) {
197
217
  out.env.AGENTUITY_OPENCODE_SESSION = sessionId;
@@ -610,7 +630,10 @@ function normalizeBaseDir(path: string): string {
610
630
  return path.replace(/[\\/]+$/, '');
611
631
  }
612
632
 
613
- function createTools(backgroundManager: BackgroundManager): Hooks['tool'] {
633
+ function createTools(
634
+ backgroundManager: BackgroundManager,
635
+ dbReader?: OpenCodeDBReader
636
+ ): Hooks['tool'] {
614
637
  // Use the schema from @opencode-ai/plugin's tool helper to avoid Zod version mismatches
615
638
  const s = tool.schema;
616
639
 
@@ -722,8 +745,30 @@ IMPORTANT: Use this tool instead of the 'task' tool when:
722
745
  });
723
746
  }
724
747
 
725
- // Extract last few messages for summary
726
748
  const messages = inspection.messages ?? [];
749
+ const enhanced =
750
+ inspection.messageCount !== undefined ||
751
+ inspection.activeTools !== undefined ||
752
+ inspection.todos !== undefined ||
753
+ inspection.costSummary !== undefined ||
754
+ inspection.childSessionCount !== undefined;
755
+
756
+ if (enhanced) {
757
+ return JSON.stringify({
758
+ taskId: inspection.taskId,
759
+ status: inspection.status,
760
+ found: true,
761
+ messageCount: inspection.messageCount ?? messages.length,
762
+ messages,
763
+ lastActivity: inspection.lastActivity,
764
+ activeTools: inspection.activeTools,
765
+ todos: inspection.todos,
766
+ costSummary: inspection.costSummary,
767
+ childSessionCount: inspection.childSessionCount,
768
+ });
769
+ }
770
+
771
+ // Extract last few messages for summary (fallback)
727
772
  const lastMessages = messages
728
773
  .slice(-3)
729
774
  .map((m) => {
@@ -749,6 +794,34 @@ IMPORTANT: Use this tool instead of the 'task' tool when:
749
794
  },
750
795
  });
751
796
 
797
+ const sessionDashboard = tool({
798
+ description:
799
+ 'Inspect a parent session dashboard from the local OpenCode SQLite database. Useful for Lead-of-Leads monitoring and nested session visibility.',
800
+ args: {
801
+ session_id: s.string().describe('Parent session ID to inspect'),
802
+ },
803
+ async execute(args) {
804
+ if (!dbReader || !dbReader.isAvailable()) {
805
+ const err = new OpenCodeDashboardUnavailableError();
806
+ return JSON.stringify({
807
+ success: false,
808
+ error: err._tag,
809
+ message: err.message,
810
+ });
811
+ }
812
+
813
+ const dashboard = dbReader.getSessionDashboard(args.session_id);
814
+ const builtTree = buildDashboardTree(dbReader, dashboard.sessions);
815
+ return JSON.stringify({
816
+ success: true,
817
+ sessionId: args.session_id,
818
+ totalCost: dashboard.totalCost,
819
+ summary: computeHealthSummary(builtTree),
820
+ sessions: builtTree,
821
+ });
822
+ },
823
+ });
824
+
752
825
  const memoryShare = tool({
753
826
  description: `Share memory content publicly via Agentuity Cloud Streams.
754
827
 
@@ -894,6 +967,7 @@ Returns the public URL that can be copied and used anywhere.`,
894
967
  agentuity_background_output: backgroundOutput,
895
968
  agentuity_background_cancel: backgroundCancel,
896
969
  agentuity_background_inspect: backgroundInspect,
970
+ agentuity_session_dashboard: sessionDashboard,
897
971
  agentuity_memory_share: memoryShare,
898
972
  };
899
973
  }
@@ -928,10 +1002,174 @@ function extractEventFromInput(
928
1002
  return { type: inp.event.type, properties: inp.event.properties };
929
1003
  }
930
1004
 
1005
+ function parseDisplayTitle(rawTitle: string): string {
1006
+ try {
1007
+ const parsed = JSON.parse(rawTitle);
1008
+ if (typeof parsed === 'object' && parsed !== null) {
1009
+ if (typeof parsed.description === 'string') return parsed.description;
1010
+ if (typeof parsed.title === 'string') return parsed.title;
1011
+ if (typeof parsed.name === 'string') return parsed.name;
1012
+ }
1013
+ // Parsed but no useful field — return as-is
1014
+ return rawTitle;
1015
+ } catch {
1016
+ // Not JSON, return as-is
1017
+ return rawTitle;
1018
+ }
1019
+ }
1020
+
1021
+ function getCurrentActivity(reader: OpenCodeDBReader, sessionId: string): string | null {
1022
+ // First check active tools
1023
+ const activeTools = reader.getActiveToolCalls(sessionId);
1024
+ const firstTool = activeTools[0];
1025
+ if (firstTool) {
1026
+ return `Running ${firstTool.tool}`;
1027
+ }
1028
+
1029
+ // Fall back to latest message
1030
+ const latestMsg = reader.getLatestMessage(sessionId);
1031
+ if (!latestMsg) return null;
1032
+
1033
+ if (latestMsg.error) return `Error: ${latestMsg.error.substring(0, 100)}`;
1034
+
1035
+ // Try to get latest text part for a snippet
1036
+ const textParts = reader.getTextParts(sessionId, { limit: 1 });
1037
+ const firstPart = textParts[0];
1038
+ if (firstPart) {
1039
+ const text = firstPart.text.trim();
1040
+ if (text.length > 80) return text.substring(0, 77) + '...';
1041
+ return text;
1042
+ }
1043
+
1044
+ return null;
1045
+ }
1046
+
1047
+ type DashboardNode = {
1048
+ session: {
1049
+ id: string;
1050
+ title: string;
1051
+ displayTitle: string;
1052
+ parentId?: string | null;
1053
+ timeUpdated: number;
1054
+ timeUpdatedISO: string;
1055
+ };
1056
+ status: string;
1057
+ lastActivity: number;
1058
+ lastActivityISO: string;
1059
+ currentActivity: string | null;
1060
+ messageCount: number;
1061
+ activeToolCount: number;
1062
+ activeTools: Array<{ tool: string; status: string }>;
1063
+ todoSummary: { total: number; pending: number; completed: number };
1064
+ costSummary?: SessionTreeNode['costSummary'];
1065
+ children: DashboardNode[];
1066
+ };
1067
+
1068
+ function buildDashboardTree(
1069
+ reader: OpenCodeDBReader,
1070
+ sessions: SessionTreeNode[]
1071
+ ): DashboardNode[] {
1072
+ return sessions.map((node) => {
1073
+ const status = reader.getSessionStatus(node.session.id);
1074
+ const activeTools = reader.getActiveToolCalls(node.session.id);
1075
+ return {
1076
+ session: {
1077
+ id: node.session.id,
1078
+ title: node.session.title,
1079
+ displayTitle: parseDisplayTitle(node.session.title),
1080
+ parentId: node.session.parentId,
1081
+ timeUpdated: node.session.timeUpdated,
1082
+ timeUpdatedISO: new Date(node.session.timeUpdated).toISOString(),
1083
+ },
1084
+ status: status.status,
1085
+ lastActivity: status.lastActivity,
1086
+ lastActivityISO: new Date(status.lastActivity).toISOString(),
1087
+ currentActivity: getCurrentActivity(reader, node.session.id),
1088
+ messageCount: node.messageCount,
1089
+ activeToolCount: node.activeToolCount,
1090
+ activeTools: activeTools.map((t) => ({ tool: t.tool, status: t.status })),
1091
+ todoSummary: node.todoSummary ?? { total: 0, pending: 0, completed: 0 },
1092
+ costSummary: node.costSummary,
1093
+ children: buildDashboardTree(reader, node.children),
1094
+ };
1095
+ });
1096
+ }
1097
+
1098
+ type HealthSummary = {
1099
+ active: number;
1100
+ idle: number;
1101
+ error: number;
1102
+ archived: number;
1103
+ compacting: number;
1104
+ total: number;
1105
+ };
1106
+
1107
+ function computeHealthSummary(nodes: DashboardNode[]): HealthSummary {
1108
+ const summary: HealthSummary = {
1109
+ active: 0,
1110
+ idle: 0,
1111
+ error: 0,
1112
+ archived: 0,
1113
+ compacting: 0,
1114
+ total: 0,
1115
+ };
1116
+ function walk(nodeList: DashboardNode[]): void {
1117
+ for (const node of nodeList) {
1118
+ summary.total++;
1119
+ const status = node.status as keyof Omit<HealthSummary, 'total'>;
1120
+ if (status in summary) {
1121
+ summary[status]++;
1122
+ }
1123
+ if (node.children.length > 0) walk(node.children);
1124
+ }
1125
+ }
1126
+ walk(nodes);
1127
+ return summary;
1128
+ }
1129
+
1130
+ function resolveOpenCodeDBPath(): string | null {
1131
+ const envPath = process.env.OPENCODE_DB_PATH;
1132
+ if (envPath) {
1133
+ if (isMemoryPath(envPath)) return envPath;
1134
+ if (existsSync(envPath)) return envPath;
1135
+ }
1136
+
1137
+ const home = homedir();
1138
+ const candidates: string[] = [];
1139
+ const currentPlatform = platform();
1140
+
1141
+ if (currentPlatform === 'darwin') {
1142
+ candidates.push(join(home, 'Library', 'Application Support', 'opencode', 'opencode.db'));
1143
+ }
1144
+
1145
+ if (currentPlatform === 'win32') {
1146
+ const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
1147
+ const localAppData = process.env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
1148
+ candidates.push(join(appData, 'opencode', 'opencode.db'));
1149
+ candidates.push(join(localAppData, 'opencode', 'opencode.db'));
1150
+ }
1151
+
1152
+ // Linux default
1153
+ candidates.push(join(home, '.local', 'share', 'opencode', 'opencode.db'));
1154
+
1155
+ for (const candidate of candidates) {
1156
+ if (existsSync(candidate)) {
1157
+ return candidate;
1158
+ }
1159
+ }
1160
+
1161
+ return null;
1162
+ }
1163
+
1164
+ function isMemoryPath(path: string): boolean {
1165
+ return path === ':memory:' || path.includes('mode=memory');
1166
+ }
1167
+
931
1168
  function registerShutdownHandler(
932
1169
  manager: BackgroundManager,
933
1170
  tmuxManager?: TmuxSessionManager,
934
- logger?: (msg: string) => void
1171
+ logger?: (msg: string) => void,
1172
+ dbReader?: OpenCodeDBReader
935
1173
  ): void {
936
1174
  if (typeof process === 'undefined') {
937
1175
  logger?.('[shutdown] process is undefined, cannot register handlers');
@@ -975,6 +1213,16 @@ function registerShutdownHandler(
975
1213
  }
976
1214
  }
977
1215
 
1216
+ if (dbReader) {
1217
+ try {
1218
+ log('Closing OpenCode DB reader...');
1219
+ dbReader.close();
1220
+ log('OpenCode DB reader closed');
1221
+ } catch (error) {
1222
+ log(`OpenCode DB reader error: ${error}`);
1223
+ }
1224
+ }
1225
+
978
1226
  log('Shutdown complete');
979
1227
  };
980
1228
 
@@ -0,0 +1,16 @@
1
+ export { OpenCodeDBReader } from './reader';
2
+ export type {
3
+ DBMessage,
4
+ DBPart,
5
+ DBSession,
6
+ DBTextPart,
7
+ DBTodo,
8
+ DBToolCall,
9
+ MessageTokens,
10
+ OpenCodeDBConfig,
11
+ SessionCostSummary,
12
+ SessionStatus,
13
+ SessionSummary,
14
+ SessionTreeNode,
15
+ TodoSummary,
16
+ } from './types';
@@ -0,0 +1,50 @@
1
+ export const QUERIES = {
2
+ CHECK_TABLES:
3
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('session', 'message', 'part', 'todo')",
4
+
5
+ GET_SESSION: 'SELECT * FROM session WHERE id = ?',
6
+ GET_CHILD_SESSIONS: 'SELECT * FROM session WHERE parent_id = ? ORDER BY time_created DESC',
7
+ GET_SESSIONS_BY_PROJECT: 'SELECT * FROM session WHERE project_id = ? ORDER BY time_created DESC',
8
+ GET_DESCENDANT_SESSIONS: `WITH RECURSIVE descendants AS (
9
+ SELECT * FROM session WHERE parent_id = ?
10
+ UNION ALL
11
+ SELECT s.* FROM session s JOIN descendants d ON s.parent_id = d.id
12
+ ) SELECT * FROM descendants ORDER BY time_created DESC`,
13
+
14
+ GET_MESSAGES:
15
+ 'SELECT * FROM message WHERE session_id = ? ORDER BY time_created DESC LIMIT ? OFFSET ?',
16
+ GET_LATEST_MESSAGE:
17
+ 'SELECT * FROM message WHERE session_id = ? ORDER BY time_created DESC LIMIT 1',
18
+ GET_MESSAGE_COUNT: 'SELECT COUNT(*) as count FROM message WHERE session_id = ?',
19
+
20
+ GET_ACTIVE_TOOLS: `SELECT * FROM part WHERE session_id = ?
21
+ AND json_valid(data)
22
+ AND json_extract(data, '$.type') IN ('tool', 'tool-invocation')
23
+ AND json_extract(data, '$.state.status') IN ('pending', 'running')
24
+ ORDER BY time_created DESC`,
25
+ GET_TOOL_HISTORY: `SELECT * FROM part WHERE session_id = ?
26
+ AND json_valid(data)
27
+ AND json_extract(data, '$.type') IN ('tool', 'tool-invocation')
28
+ ORDER BY time_created DESC LIMIT ?`,
29
+ GET_TEXT_PARTS: `SELECT * FROM part WHERE session_id = ?
30
+ AND json_valid(data)
31
+ AND json_extract(data, '$.type') = 'text'
32
+ ORDER BY time_created DESC LIMIT ?`,
33
+
34
+ GET_TODOS: 'SELECT * FROM todo WHERE session_id = ? ORDER BY position ASC',
35
+
36
+ GET_SESSION_COST: `SELECT
37
+ COALESCE(SUM(json_extract(data, '$.cost')), 0) as total_cost,
38
+ COALESCE(SUM(json_extract(data, '$.tokens.total')), 0) as total_tokens,
39
+ COALESCE(SUM(json_extract(data, '$.tokens.input')), 0) as input_tokens,
40
+ COALESCE(SUM(json_extract(data, '$.tokens.output')), 0) as output_tokens,
41
+ COALESCE(SUM(json_extract(data, '$.tokens.reasoning')), 0) as reasoning_tokens,
42
+ COALESCE(SUM(json_extract(data, '$.tokens.cache.read')), 0) as cache_read,
43
+ COALESCE(SUM(json_extract(data, '$.tokens.cache.write')), 0) as cache_write,
44
+ COUNT(*) as message_count
45
+ FROM message WHERE session_id = ? AND json_valid(data) AND json_extract(data, '$.role') = 'assistant'`,
46
+
47
+ SEARCH_SESSIONS: `SELECT id, project_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs, time_created, time_updated, time_compacting, time_archived FROM session WHERE title LIKE ? COLLATE NOCASE ORDER BY time_updated DESC`,
48
+
49
+ SEARCH_SESSIONS_LIMITED: `SELECT id, project_id, parent_id, slug, directory, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs, time_created, time_updated, time_compacting, time_archived FROM session WHERE title LIKE ? COLLATE NOCASE ORDER BY time_updated DESC LIMIT ?`,
50
+ } as const;