hungry-ghost-hive 0.49.0 → 0.50.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.
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +16 -6
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +9 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
- package/dist/cli/commands/nuke.d.ts.map +1 -1
- package/dist/cli/commands/nuke.js +7 -7
- package/dist/cli/commands/nuke.js.map +1 -1
- package/dist/cli/commands/nuke.test.js +36 -24
- package/dist/cli/commands/nuke.test.js.map +1 -1
- package/dist/cli/commands/req.d.ts +1 -1
- package/dist/cli/commands/req.d.ts.map +1 -1
- package/dist/cli/commands/req.js +11 -3
- package/dist/cli/commands/req.js.map +1 -1
- package/dist/cli/dashboard/index.d.ts.map +1 -1
- package/dist/cli/dashboard/index.js +35 -8
- package/dist/cli/dashboard/index.js.map +1 -1
- package/dist/cli/dashboard/panels/activity.js +4 -1
- package/dist/cli/dashboard/panels/activity.js.map +1 -1
- package/dist/cli/dashboard/panels/stories.d.ts +3 -3
- package/dist/cli/dashboard/panels/stories.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/stories.js +1 -2
- package/dist/cli/dashboard/panels/stories.js.map +1 -1
- package/dist/db/postgres-provider.d.ts +1 -1
- package/dist/db/postgres-provider.d.ts.map +1 -1
- package/dist/db/postgres-provider.js +118 -11
- package/dist/db/postgres-provider.js.map +1 -1
- package/dist/db/queries/logs.d.ts.map +1 -1
- package/dist/db/queries/logs.js +24 -7
- package/dist/db/queries/logs.js.map +1 -1
- package/dist/db/queries/requirements.js +3 -3
- package/dist/db/queries/requirements.js.map +1 -1
- package/dist/orchestrator/scheduler.js +1 -1
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/utils/with-hive-context.d.ts.map +1 -1
- package/dist/utils/with-hive-context.js +6 -2
- package/dist/utils/with-hive-context.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.ts +23 -6
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +10 -1
- package/src/cli/commands/nuke.test.ts +39 -24
- package/src/cli/commands/nuke.ts +12 -15
- package/src/cli/commands/req.ts +13 -3
- package/src/cli/dashboard/index.ts +38 -8
- package/src/cli/dashboard/panels/activity.ts +5 -2
- package/src/cli/dashboard/panels/stories.ts +8 -6
- package/src/db/postgres-provider.ts +130 -11
- package/src/db/queries/logs.ts +27 -10
- package/src/db/queries/requirements.ts +3 -3
- package/src/orchestrator/scheduler.ts +1 -1
- package/src/utils/with-hive-context.ts +6 -2
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import blessed from 'blessed';
|
|
4
4
|
import { appendFileSync, existsSync, renameSync, statSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { loadConfig } from '../../config/loader.js';
|
|
6
7
|
import { getReadOnlyDatabase, type ReadOnlyDatabaseClient } from '../../db/client.js';
|
|
8
|
+
import { createPostgresProvider } from '../../db/postgres-provider.js';
|
|
7
9
|
import type { DatabaseProvider } from '../../db/provider.js';
|
|
8
10
|
import { getAllRequirements } from '../../db/queries/requirements.js';
|
|
9
|
-
import { findHiveRoot, getHivePaths } from '../../utils/paths.js';
|
|
11
|
+
import { findHiveRoot, getHivePaths, getWorkspaceId } from '../../utils/paths.js';
|
|
10
12
|
import { getVersion } from '../../utils/version.js';
|
|
11
13
|
import { createActivityPanel, updateActivityPanel } from './panels/activity.js';
|
|
12
14
|
import { createAgentsPanel, updateAgentsPanel } from './panels/agents.js';
|
|
@@ -56,11 +58,39 @@ export async function startDashboard(options: DashboardOptions = {}): Promise<vo
|
|
|
56
58
|
|
|
57
59
|
const paths = getHivePaths(root);
|
|
58
60
|
const dbPath = join(paths.hiveDir, 'hive.db');
|
|
59
|
-
|
|
61
|
+
let isDistributed = false;
|
|
62
|
+
try {
|
|
63
|
+
const config = loadConfig(paths.hiveDir);
|
|
64
|
+
isDistributed = config.distributed === true;
|
|
65
|
+
} catch {
|
|
66
|
+
// fallback: not distributed
|
|
67
|
+
}
|
|
60
68
|
debugLog(
|
|
61
69
|
`Dashboard starting - root: ${root}, hiveDir: ${paths.hiveDir}, distributed: ${isDistributed}`
|
|
62
70
|
);
|
|
63
|
-
|
|
71
|
+
|
|
72
|
+
async function createDbClient(): Promise<ReadOnlyDatabaseClient> {
|
|
73
|
+
if (isDistributed) {
|
|
74
|
+
const workspaceId = getWorkspaceId(paths);
|
|
75
|
+
if (!workspaceId) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Distributed mode but workspace.id is missing. Re-run "hive init --distributed".'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const envPath = join(root!, '.env');
|
|
81
|
+
const provider = await createPostgresProvider(workspaceId, envPath);
|
|
82
|
+
return {
|
|
83
|
+
db: null as never,
|
|
84
|
+
provider,
|
|
85
|
+
close: () => {
|
|
86
|
+
provider.close();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return getReadOnlyDatabase(paths.hiveDir);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let db: ReadOnlyDatabaseClient = await createDbClient();
|
|
64
94
|
let lastDbMtime = isDistributed ? 0 : statSync(dbPath).mtimeMs;
|
|
65
95
|
const refreshInterval = options.refreshInterval || 5000;
|
|
66
96
|
const version = getVersion();
|
|
@@ -99,7 +129,7 @@ export async function startDashboard(options: DashboardOptions = {}): Promise<vo
|
|
|
99
129
|
|
|
100
130
|
// Create panels
|
|
101
131
|
const agentsPanel = createAgentsPanel(screen, db.provider, pauseRefresh, resumeRefresh);
|
|
102
|
-
const storiesPanel = createStoriesPanel(screen, db.
|
|
132
|
+
const storiesPanel = createStoriesPanel(screen, db.provider);
|
|
103
133
|
const pipelinePanel = createPipelinePanel(screen, db.provider);
|
|
104
134
|
const activityPanel = createActivityPanel(screen, db.provider);
|
|
105
135
|
const mergeQueuePanel = createMergeQueuePanel(screen, db.provider);
|
|
@@ -144,9 +174,9 @@ export async function startDashboard(options: DashboardOptions = {}): Promise<vo
|
|
|
144
174
|
|
|
145
175
|
if (shouldReload) {
|
|
146
176
|
debugLog(`Database changed - reloading from ${paths.hiveDir}`);
|
|
147
|
-
const newDb = await
|
|
177
|
+
const newDb = await createDbClient();
|
|
148
178
|
try {
|
|
149
|
-
db.
|
|
179
|
+
db.close();
|
|
150
180
|
} catch (_error) {
|
|
151
181
|
/* ignore close errors */
|
|
152
182
|
}
|
|
@@ -161,7 +191,7 @@ export async function startDashboard(options: DashboardOptions = {}): Promise<vo
|
|
|
161
191
|
);
|
|
162
192
|
|
|
163
193
|
await updateAgentsPanel(agentsPanel, db.provider);
|
|
164
|
-
await updateStoriesPanel(storiesPanel, db.
|
|
194
|
+
await updateStoriesPanel(storiesPanel, db.provider);
|
|
165
195
|
await updatePipelinePanel(pipelinePanel, db.provider);
|
|
166
196
|
await updateActivityPanel(activityPanel, db.provider);
|
|
167
197
|
await updateMergeQueuePanel(mergeQueuePanel, db.provider);
|
|
@@ -191,7 +221,7 @@ export async function startDashboard(options: DashboardOptions = {}): Promise<vo
|
|
|
191
221
|
screen.key(['q', 'C-c'], () => {
|
|
192
222
|
if (currentTimeout) clearTimeout(currentTimeout);
|
|
193
223
|
try {
|
|
194
|
-
db.
|
|
224
|
+
db.close();
|
|
195
225
|
} catch (_error) {
|
|
196
226
|
/* ignore */
|
|
197
227
|
}
|
|
@@ -60,8 +60,11 @@ export async function updateActivityPanel(
|
|
|
60
60
|
box.setScrollPerc(100);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function formatTimestamp(timestamp: string): string {
|
|
64
|
-
|
|
63
|
+
function formatTimestamp(timestamp: string | Date): string {
|
|
64
|
+
if (timestamp instanceof Date) {
|
|
65
|
+
return timestamp.toISOString().substring(11, 19);
|
|
66
|
+
}
|
|
67
|
+
return String(timestamp).substring(11, 19);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function formatEventType(event: string): string {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
import blessed, { type Widgets } from 'blessed';
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
4
|
+
import type { StoryRow } from '../../../db/client.js';
|
|
5
|
+
import type { DatabaseProvider } from '../../../db/provider.js';
|
|
6
6
|
|
|
7
|
-
export function createStoriesPanel(
|
|
7
|
+
export function createStoriesPanel(
|
|
8
|
+
screen: Widgets.Screen,
|
|
9
|
+
db: DatabaseProvider
|
|
10
|
+
): Widgets.ListTableElement {
|
|
8
11
|
const table = blessed.listtable({
|
|
9
12
|
parent: screen,
|
|
10
13
|
top: '30%',
|
|
@@ -32,10 +35,9 @@ export function createStoriesPanel(screen: Widgets.Screen, db: Database): Widget
|
|
|
32
35
|
|
|
33
36
|
export async function updateStoriesPanel(
|
|
34
37
|
table: Widgets.ListTableElement,
|
|
35
|
-
db:
|
|
38
|
+
db: DatabaseProvider
|
|
36
39
|
): Promise<void> {
|
|
37
|
-
const stories = queryAll<StoryRow>(
|
|
38
|
-
db,
|
|
40
|
+
const stories = await db.queryAll<StoryRow>(
|
|
39
41
|
`
|
|
40
42
|
SELECT * FROM stories
|
|
41
43
|
ORDER BY
|
|
@@ -83,6 +83,66 @@ function detectTable(sql: string): string | null {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Detect the alias (or table name) for the primary table in a SQL statement.
|
|
88
|
+
* For `SELECT ... FROM stories s LEFT JOIN ...`, returns "s".
|
|
89
|
+
* For `SELECT ... FROM stories LEFT JOIN ...`, returns "stories".
|
|
90
|
+
* For UPDATE/DELETE, returns the table name or alias.
|
|
91
|
+
*/
|
|
92
|
+
function detectTableQualifier(sql: string): string | null {
|
|
93
|
+
const normalized = sql.replace(/\s+/g, ' ').trim();
|
|
94
|
+
|
|
95
|
+
// SELECT ... FROM table [alias] [JOIN|WHERE|ORDER|GROUP|HAVING|LIMIT|,|)]
|
|
96
|
+
const selectMatch = normalized.match(/FROM\s+(\w+)(?:\s+(?:AS\s+)?(\w+))?/i);
|
|
97
|
+
if (selectMatch) {
|
|
98
|
+
const alias = selectMatch[2];
|
|
99
|
+
const table = selectMatch[1];
|
|
100
|
+
// Only treat as alias if the next word is not a SQL keyword
|
|
101
|
+
if (alias) {
|
|
102
|
+
const upper = alias.toUpperCase();
|
|
103
|
+
const keywords = new Set([
|
|
104
|
+
'WHERE',
|
|
105
|
+
'LEFT',
|
|
106
|
+
'RIGHT',
|
|
107
|
+
'INNER',
|
|
108
|
+
'OUTER',
|
|
109
|
+
'CROSS',
|
|
110
|
+
'JOIN',
|
|
111
|
+
'ON',
|
|
112
|
+
'ORDER',
|
|
113
|
+
'GROUP',
|
|
114
|
+
'HAVING',
|
|
115
|
+
'LIMIT',
|
|
116
|
+
'OFFSET',
|
|
117
|
+
'UNION',
|
|
118
|
+
'EXCEPT',
|
|
119
|
+
'INTERSECT',
|
|
120
|
+
'FOR',
|
|
121
|
+
'SET',
|
|
122
|
+
'VALUES',
|
|
123
|
+
]);
|
|
124
|
+
if (!keywords.has(upper)) {
|
|
125
|
+
return alias;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return table;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// UPDATE table [alias]
|
|
132
|
+
const updateMatch = normalized.match(/UPDATE\s+(\w+)(?:\s+(?:AS\s+)?(\w+))?/i);
|
|
133
|
+
if (updateMatch) {
|
|
134
|
+
return updateMatch[2] || updateMatch[1];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// DELETE FROM table [alias]
|
|
138
|
+
const deleteMatch = normalized.match(/DELETE\s+FROM\s+(\w+)(?:\s+(?:AS\s+)?(\w+))?/i);
|
|
139
|
+
if (deleteMatch) {
|
|
140
|
+
return deleteMatch[2] || deleteMatch[1];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
86
146
|
/**
|
|
87
147
|
* Check if a SQL statement needs workspace_id injection.
|
|
88
148
|
*/
|
|
@@ -123,6 +183,8 @@ function injectInsertWorkspaceId(
|
|
|
123
183
|
/**
|
|
124
184
|
* Inject workspace_id into SELECT/UPDATE/DELETE WHERE clauses.
|
|
125
185
|
* Adds `AND workspace_id = ?` to existing WHERE, or `WHERE workspace_id = ?` if none.
|
|
186
|
+
* When the query uses JOINs, qualifies workspace_id with the primary table alias
|
|
187
|
+
* to avoid ambiguous column references.
|
|
126
188
|
*/
|
|
127
189
|
function injectWhereWorkspaceId(
|
|
128
190
|
sql: string,
|
|
@@ -136,18 +198,46 @@ function injectWhereWorkspaceId(
|
|
|
136
198
|
return { sql, params };
|
|
137
199
|
}
|
|
138
200
|
|
|
201
|
+
// Determine if we need to qualify workspace_id (JOIN queries have ambiguous columns)
|
|
202
|
+
const hasJoin = /\bJOIN\b/i.test(normalized);
|
|
203
|
+
const qualifier = hasJoin ? detectTableQualifier(sql) : null;
|
|
204
|
+
const wsCol = qualifier ? `${qualifier}.workspace_id` : 'workspace_id';
|
|
205
|
+
|
|
139
206
|
const upperSql = normalized.toUpperCase();
|
|
140
207
|
|
|
141
208
|
// Find WHERE clause position
|
|
142
209
|
const whereIndex = upperSql.indexOf(' WHERE ');
|
|
143
210
|
|
|
144
211
|
if (whereIndex !== -1) {
|
|
145
|
-
//
|
|
212
|
+
// Append workspace_id condition at the end of the WHERE clause.
|
|
213
|
+
// We must NOT prepend because params are positional — SET clause params
|
|
214
|
+
// come before WHERE clause params, and prepending would shift them.
|
|
146
215
|
const before = normalized.substring(0, whereIndex + 7); // includes " WHERE "
|
|
147
216
|
const after = normalized.substring(whereIndex + 7);
|
|
217
|
+
|
|
218
|
+
// Find any trailing clauses (ORDER BY, GROUP BY, etc.) after the WHERE
|
|
219
|
+
const upperAfter = after.toUpperCase();
|
|
220
|
+
const trailingPatterns = [
|
|
221
|
+
' ORDER BY',
|
|
222
|
+
' GROUP BY',
|
|
223
|
+
' HAVING',
|
|
224
|
+
' LIMIT',
|
|
225
|
+
' OFFSET',
|
|
226
|
+
' FOR UPDATE',
|
|
227
|
+
];
|
|
228
|
+
let trailingPos = after.length;
|
|
229
|
+
for (const pattern of trailingPatterns) {
|
|
230
|
+
const idx = upperAfter.indexOf(pattern);
|
|
231
|
+
if (idx !== -1 && idx < trailingPos) {
|
|
232
|
+
trailingPos = idx;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const whereBody = after.substring(0, trailingPos);
|
|
237
|
+
const trailing = after.substring(trailingPos);
|
|
148
238
|
return {
|
|
149
|
-
sql: `${before}
|
|
150
|
-
params: [
|
|
239
|
+
sql: `${before}${whereBody} AND ${wsCol} = ?${trailing}`,
|
|
240
|
+
params: [...params, workspaceId],
|
|
151
241
|
};
|
|
152
242
|
}
|
|
153
243
|
|
|
@@ -163,9 +253,16 @@ function injectWhereWorkspaceId(
|
|
|
163
253
|
|
|
164
254
|
const before = normalized.substring(0, insertPos);
|
|
165
255
|
const after = normalized.substring(insertPos);
|
|
256
|
+
|
|
257
|
+
// Count how many ? appear before the insertion point to determine where
|
|
258
|
+
// in the params array the workspace_id value should be spliced in.
|
|
259
|
+
const questionsBefore = (before.match(/\?/g) || []).length;
|
|
260
|
+
const newParams = [...params];
|
|
261
|
+
newParams.splice(questionsBefore, 0, workspaceId);
|
|
262
|
+
|
|
166
263
|
return {
|
|
167
|
-
sql: `${before} WHERE
|
|
168
|
-
params:
|
|
264
|
+
sql: `${before} WHERE ${wsCol} = ?${after}`,
|
|
265
|
+
params: newParams,
|
|
169
266
|
};
|
|
170
267
|
}
|
|
171
268
|
|
|
@@ -191,6 +288,17 @@ function loadPgMigration(migrationName: string): string {
|
|
|
191
288
|
|
|
192
289
|
const PG_MIGRATIONS = [{ name: '001-full-schema.sql' }];
|
|
193
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Strip SQLite-specific syntax that Postgres does not understand.
|
|
293
|
+
*/
|
|
294
|
+
function sanitizeForPostgres(sql: string): string {
|
|
295
|
+
// Remove COLLATE NOCASE (SQLite case-insensitive collation)
|
|
296
|
+
let result = sql.replace(/\s+COLLATE\s+NOCASE/gi, '');
|
|
297
|
+
// Convert INSERT OR IGNORE to INSERT ... ON CONFLICT DO NOTHING
|
|
298
|
+
result = result.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, 'INSERT INTO');
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
194
302
|
/**
|
|
195
303
|
* Postgres implementation of DatabaseProvider using node-postgres (pg).
|
|
196
304
|
* All queries are automatically scoped by workspace_id for multi-tenant isolation.
|
|
@@ -242,12 +350,16 @@ export class PostgresProvider implements WritableDatabaseProvider {
|
|
|
242
350
|
}
|
|
243
351
|
|
|
244
352
|
async queryAll<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
245
|
-
let finalSql = sql;
|
|
353
|
+
let finalSql = sanitizeForPostgres(sql);
|
|
246
354
|
let finalParams = params;
|
|
247
355
|
|
|
248
356
|
if (needsWorkspaceScope(sql)) {
|
|
249
357
|
const normalized = sql.replace(/\s+/g, ' ').trim().toUpperCase();
|
|
250
|
-
if (
|
|
358
|
+
if (normalized.startsWith('INSERT')) {
|
|
359
|
+
const result = injectInsertWorkspaceId(finalSql, this.workspaceId, finalParams);
|
|
360
|
+
finalSql = result.sql;
|
|
361
|
+
finalParams = result.params;
|
|
362
|
+
} else {
|
|
251
363
|
const result = injectWhereWorkspaceId(finalSql, this.workspaceId, finalParams);
|
|
252
364
|
finalSql = result.sql;
|
|
253
365
|
finalParams = result.params;
|
|
@@ -265,7 +377,7 @@ export class PostgresProvider implements WritableDatabaseProvider {
|
|
|
265
377
|
}
|
|
266
378
|
|
|
267
379
|
async run(sql: string, params: unknown[] = []): Promise<void> {
|
|
268
|
-
let finalSql = sql;
|
|
380
|
+
let finalSql = sanitizeForPostgres(sql);
|
|
269
381
|
let finalParams = params;
|
|
270
382
|
|
|
271
383
|
if (needsWorkspaceScope(sql)) {
|
|
@@ -340,11 +452,18 @@ export class PostgresProvider implements WritableDatabaseProvider {
|
|
|
340
452
|
* Create a PostgresProvider from the HIVE_DATABASE_URL environment variable.
|
|
341
453
|
* Loads .env file via dotenv if available.
|
|
342
454
|
*/
|
|
343
|
-
export async function createPostgresProvider(
|
|
344
|
-
|
|
455
|
+
export async function createPostgresProvider(
|
|
456
|
+
workspaceId: string,
|
|
457
|
+
envPath?: string
|
|
458
|
+
): Promise<PostgresProvider> {
|
|
459
|
+
// Load .env file if dotenv is available — use workspace root if provided
|
|
345
460
|
try {
|
|
346
461
|
const dotenv = await import('dotenv');
|
|
347
|
-
|
|
462
|
+
if (envPath) {
|
|
463
|
+
dotenv.config({ path: envPath });
|
|
464
|
+
} else {
|
|
465
|
+
dotenv.config();
|
|
466
|
+
}
|
|
348
467
|
} catch {
|
|
349
468
|
// dotenv not available, rely on environment variables
|
|
350
469
|
}
|
package/src/db/queries/logs.ts
CHANGED
|
@@ -92,6 +92,17 @@ function inferAgentType(
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
async function getAgentColumnNames(provider: DatabaseProvider): Promise<Set<string>> {
|
|
95
|
+
try {
|
|
96
|
+
// Try Postgres information_schema first
|
|
97
|
+
const pgRows = await provider.queryAll<{ column_name: string }>(
|
|
98
|
+
"SELECT column_name FROM information_schema.columns WHERE table_name = 'agents'"
|
|
99
|
+
);
|
|
100
|
+
if (pgRows.length > 0) {
|
|
101
|
+
return new Set(pgRows.map(r => r.column_name));
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Fall back to SQLite PRAGMA
|
|
105
|
+
}
|
|
95
106
|
const rows = await provider.queryAll<{ name: string }>('PRAGMA table_info(agents)');
|
|
96
107
|
const columnNames = new Set<string>();
|
|
97
108
|
for (const row of rows) {
|
|
@@ -133,13 +144,18 @@ async function ensureLogAgentExists(provider: DatabaseProvider, agentId: string)
|
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
const placeholders = insertColumns.map(() => '?').join(', ');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
try {
|
|
148
|
+
await provider.run(
|
|
149
|
+
`
|
|
150
|
+
INSERT INTO agents (${insertColumns.join(', ')})
|
|
151
|
+
VALUES (${placeholders})
|
|
152
|
+
ON CONFLICT DO NOTHING
|
|
153
|
+
`,
|
|
154
|
+
insertValues
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore insert conflicts — agent row already exists
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
async function resolveLogAgentId(provider: DatabaseProvider, rawAgentId: string): Promise<string> {
|
|
@@ -188,10 +204,13 @@ export async function createLog(
|
|
|
188
204
|
const resolvedAgentId = await resolveLogAgentId(provider, input.agentId);
|
|
189
205
|
const resolvedStoryId = await resolveLogStoryId(provider, input.storyId);
|
|
190
206
|
|
|
191
|
-
|
|
207
|
+
// Use queryOne with RETURNING to get the inserted id in a single statement.
|
|
208
|
+
// RETURNING is supported by both SQLite (3.35+) and Postgres.
|
|
209
|
+
const result = await provider.queryOne<{ id: number }>(
|
|
192
210
|
`
|
|
193
211
|
INSERT INTO agent_logs (agent_id, story_id, event_type, status, message, metadata, timestamp)
|
|
194
212
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
213
|
+
RETURNING id
|
|
195
214
|
`,
|
|
196
215
|
[
|
|
197
216
|
resolvedAgentId,
|
|
@@ -204,8 +223,6 @@ export async function createLog(
|
|
|
204
223
|
]
|
|
205
224
|
);
|
|
206
225
|
|
|
207
|
-
// Get the last inserted row
|
|
208
|
-
const result = await provider.queryOne<{ id: number }>('SELECT last_insert_rowid() as id');
|
|
209
226
|
return (await getLogById(provider, result?.id || 0))!;
|
|
210
227
|
}
|
|
211
228
|
|
|
@@ -77,7 +77,7 @@ export async function getRequirementById(
|
|
|
77
77
|
|
|
78
78
|
export async function getAllRequirements(provider: DatabaseProvider): Promise<RequirementRow[]> {
|
|
79
79
|
return await provider.queryAll<RequirementRow>(
|
|
80
|
-
'SELECT * FROM requirements ORDER BY created_at DESC,
|
|
80
|
+
'SELECT * FROM requirements ORDER BY created_at DESC, id DESC'
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -86,7 +86,7 @@ export async function getRequirementsByStatus(
|
|
|
86
86
|
status: RequirementStatus
|
|
87
87
|
): Promise<RequirementRow[]> {
|
|
88
88
|
return await provider.queryAll<RequirementRow>(
|
|
89
|
-
'SELECT * FROM requirements WHERE status = ? ORDER BY created_at DESC,
|
|
89
|
+
'SELECT * FROM requirements WHERE status = ? ORDER BY created_at DESC, id DESC',
|
|
90
90
|
[status]
|
|
91
91
|
);
|
|
92
92
|
}
|
|
@@ -97,7 +97,7 @@ export async function getPendingRequirements(
|
|
|
97
97
|
return await provider.queryAll<RequirementRow>(`
|
|
98
98
|
SELECT * FROM requirements
|
|
99
99
|
WHERE status IN ('pending', 'planning', 'in_progress')
|
|
100
|
-
ORDER BY created_at,
|
|
100
|
+
ORDER BY created_at, id
|
|
101
101
|
`);
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -1143,7 +1143,7 @@ export class Scheduler {
|
|
|
1143
1143
|
*/
|
|
1144
1144
|
private async isGodmodeActive(): Promise<boolean> {
|
|
1145
1145
|
const activeRequirements = await this.provider.queryAll<RequirementRow>(
|
|
1146
|
-
`SELECT * FROM requirements WHERE status IN ('planning', 'planned', 'in_progress') AND godmode =
|
|
1146
|
+
`SELECT * FROM requirements WHERE status IN ('planning', 'planned', 'in_progress') AND godmode = true`
|
|
1147
1147
|
);
|
|
1148
1148
|
return activeRequirements.length > 0;
|
|
1149
1149
|
}
|
|
@@ -125,7 +125,9 @@ async function withDistributedHiveContext<T>(
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
const
|
|
128
|
+
const { join } = await import('path');
|
|
129
|
+
const envPath = join(root, '.env');
|
|
130
|
+
const provider = await createPostgresProvider(workspaceId, envPath);
|
|
129
131
|
// Create a DatabaseClient-compatible wrapper around the Postgres provider
|
|
130
132
|
const db: DatabaseClient = {
|
|
131
133
|
db: null as never, // No sql.js database in distributed mode
|
|
@@ -178,7 +180,9 @@ async function withDistributedReadOnlyHiveContext<T>(
|
|
|
178
180
|
);
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
const
|
|
183
|
+
const { join } = await import('path');
|
|
184
|
+
const envPath = join(root, '.env');
|
|
185
|
+
const provider = await createPostgresProvider(workspaceId, envPath);
|
|
182
186
|
const db: ReadOnlyDatabaseClient = {
|
|
183
187
|
db: null as never, // No sql.js database in distributed mode
|
|
184
188
|
provider,
|