vgxness 0.1.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/LICENSE +9 -0
- package/README.md +110 -0
- package/dist/agents/agent-activation-service.js +144 -0
- package/dist/agents/agent-registry-service.js +46 -0
- package/dist/agents/agent-resolver.js +249 -0
- package/dist/agents/agent-seed-service.js +146 -0
- package/dist/agents/manager-profile-overlay-service.js +34 -0
- package/dist/agents/profile-model-routing.js +26 -0
- package/dist/agents/renderers/claude-renderer.js +98 -0
- package/dist/agents/renderers/index.js +16 -0
- package/dist/agents/renderers/json-renderer.js +87 -0
- package/dist/agents/renderers/opencode-renderer.js +100 -0
- package/dist/agents/renderers/provider-adapter.js +6 -0
- package/dist/agents/repositories/agents.js +185 -0
- package/dist/agents/repositories/manager-profile-overlays.js +81 -0
- package/dist/agents/schema.js +1 -0
- package/dist/cli/dashboard-operational-read-models.js +153 -0
- package/dist/cli/dashboard-renderer.js +109 -0
- package/dist/cli/dashboard-screen-renderers.js +332 -0
- package/dist/cli/dashboard-tui-read-model.js +71 -0
- package/dist/cli/dashboard-tui-state.js +218 -0
- package/dist/cli/dispatcher.js +2880 -0
- package/dist/cli/index.js +27 -0
- package/dist/cli/interactive-dashboard.js +29 -0
- package/dist/cli/mcp-start-path.js +21 -0
- package/dist/cli/setup-status-renderer.js +29 -0
- package/dist/cli/setup-wizard-read-model.js +56 -0
- package/dist/cli/setup-wizard-renderer.js +148 -0
- package/dist/cli/setup-wizard-state.js +82 -0
- package/dist/cli/tui-render-helpers.js +192 -0
- package/dist/export/redaction.js +71 -0
- package/dist/harness/tools/agents.js +245 -0
- package/dist/harness/tools/memory.js +29 -0
- package/dist/mcp/client-install-opencode-contract.js +227 -0
- package/dist/mcp/client-install-opencode.js +194 -0
- package/dist/mcp/client-setup-preview.js +38 -0
- package/dist/mcp/control-plane.js +175 -0
- package/dist/mcp/doctor.js +193 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/opencode-default-agent-config.js +156 -0
- package/dist/mcp/opencode-visibility.js +102 -0
- package/dist/mcp/schema.js +234 -0
- package/dist/mcp/stdio-server.js +56 -0
- package/dist/mcp/validation.js +761 -0
- package/dist/memory/import/dry-run-planner.js +58 -0
- package/dist/memory/import/index.js +3 -0
- package/dist/memory/import/observation-writer.js +220 -0
- package/dist/memory/import/package.js +178 -0
- package/dist/memory/memory-service.js +126 -0
- package/dist/memory/repositories/artifacts.js +41 -0
- package/dist/memory/repositories/observations.js +133 -0
- package/dist/memory/repositories/sessions.js +105 -0
- package/dist/memory/repositories/traces.js +58 -0
- package/dist/memory/schema.js +1 -0
- package/dist/memory/search.js +11 -0
- package/dist/memory/sqlite/database.js +97 -0
- package/dist/memory/sqlite/migrations/001_initial.sql +128 -0
- package/dist/memory/sqlite/migrations/002_observation_revisions.sql +14 -0
- package/dist/memory/sqlite/migrations/003_agent_registry.sql +26 -0
- package/dist/memory/sqlite/migrations/004_run_runtime.sql +62 -0
- package/dist/memory/sqlite/migrations/005_run_approvals.sql +20 -0
- package/dist/memory/sqlite/migrations/006_run_operation_attempts.sql +32 -0
- package/dist/memory/sqlite/migrations/007_abandoned_operation_attempts.sql +46 -0
- package/dist/memory/sqlite/migrations/008_run_execution_plan_events.sql +105 -0
- package/dist/memory/sqlite/migrations/009_multiple_operation_attempts.sql +73 -0
- package/dist/memory/sqlite/migrations/010_skill_registry.sql +66 -0
- package/dist/memory/sqlite/migrations/011_skill_usage_resolution_outcomes.sql +21 -0
- package/dist/memory/sqlite/migrations/012_skill_improvement_proposals.sql +37 -0
- package/dist/memory/sqlite/migrations/013_skill_evaluation_scenarios.sql +43 -0
- package/dist/memory/sqlite/migrations/014_manager_profile_overlays.sql +14 -0
- package/dist/memory/storage-paths.js +72 -0
- package/dist/orchestrator/natural-language-planner.js +191 -0
- package/dist/orchestrator/schema.js +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/policy-evaluator.js +109 -0
- package/dist/permissions/schema.js +1 -0
- package/dist/providers/opencode/injection-preview.js +134 -0
- package/dist/providers/opencode/manager-payload.js +129 -0
- package/dist/runs/execution-planning.js +117 -0
- package/dist/runs/operation-execution.js +1 -0
- package/dist/runs/operation-retry.js +124 -0
- package/dist/runs/repositories/runs.js +611 -0
- package/dist/runs/run-insights.js +145 -0
- package/dist/runs/run-service.js +713 -0
- package/dist/runs/run-snapshot-export-service.js +31 -0
- package/dist/runs/sandbox-process-execution.js +218 -0
- package/dist/runs/sandbox-worktree-planning.js +59 -0
- package/dist/runs/schema.js +1 -0
- package/dist/sdd/artifact-portability-service.js +118 -0
- package/dist/sdd/schema.js +17 -0
- package/dist/sdd/sdd-workflow-service.js +217 -0
- package/dist/setup/backup-rollback-service.js +76 -0
- package/dist/setup/index.js +3 -0
- package/dist/setup/providers/antigravity-setup-adapter.js +18 -0
- package/dist/setup/providers/claude-setup-adapter.js +30 -0
- package/dist/setup/providers/custom-setup-adapter.js +18 -0
- package/dist/setup/providers/index.js +6 -0
- package/dist/setup/providers/opencode-setup-adapter.js +104 -0
- package/dist/setup/providers/provider-setup-adapter.js +15 -0
- package/dist/setup/providers/provider-setup-registry.js +11 -0
- package/dist/setup/schema.js +1 -0
- package/dist/setup/setup-defaults.js +11 -0
- package/dist/setup/setup-lifecycle-service.js +175 -0
- package/dist/setup/setup-plan.js +105 -0
- package/dist/skills/repositories/skill-evaluation-scenarios.js +289 -0
- package/dist/skills/repositories/skill-improvement-proposals.js +288 -0
- package/dist/skills/repositories/skills.js +430 -0
- package/dist/skills/schema.js +1 -0
- package/dist/skills/skill-payload.js +94 -0
- package/dist/skills/skill-registry-service.js +92 -0
- package/dist/skills/skill-resolver.js +191 -0
- package/dist/workflows/command-allowlist-adapter.js +70 -0
- package/dist/workflows/schema.js +4 -0
- package/dist/workflows/workflow-executor.js +345 -0
- package/dist/workflows/workflow-registry.js +66 -0
- package/docs/architecture.md +698 -0
- package/docs/cli.md +741 -0
- package/docs/funcionamiento-del-sistema.md +868 -0
- package/docs/harness-gap-analysis.md +229 -0
- package/docs/prd.md +372 -0
- package/package.json +57 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { normalizeLimit, toFtsMatchQuery } from '../search.js';
|
|
3
|
+
export class ObservationRepository {
|
|
4
|
+
db;
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
save(input) {
|
|
9
|
+
const result = this.db.transaction(() => {
|
|
10
|
+
const existing = input.topicKey ? this.topic(input) : undefined;
|
|
11
|
+
const id = existing?.id ?? randomUUID();
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
if (existing)
|
|
14
|
+
this.db.connection.prepare('UPDATE observations SET title=@title, content=@content, updated_at=@updatedAt WHERE id=@id').run({ id, title: input.title, content: input.content, updatedAt: now });
|
|
15
|
+
else
|
|
16
|
+
this.db.connection.prepare(`INSERT INTO observations(id, project, scope, type, title, content, topic_key, created_at, updated_at)
|
|
17
|
+
VALUES (@id, @project, @scope, @type, @title, @content, @topicKey, @createdAt, @updatedAt)`).run({ ...input, id, topicKey: input.topicKey ?? null, createdAt: now, updatedAt: now });
|
|
18
|
+
this.appendRevision(id, input.topicKey === undefined
|
|
19
|
+
? { type: input.type, title: input.title, content: input.content, createdAt: now }
|
|
20
|
+
: { type: input.type, title: input.title, content: input.content, topicKey: input.topicKey, createdAt: now });
|
|
21
|
+
const found = this.getById(id);
|
|
22
|
+
if (!found.ok)
|
|
23
|
+
throw new Error(found.error.message);
|
|
24
|
+
return found.value;
|
|
25
|
+
});
|
|
26
|
+
return result.ok ? result : fail(result.error.message, result.error.cause);
|
|
27
|
+
}
|
|
28
|
+
getById(id) {
|
|
29
|
+
try {
|
|
30
|
+
const row = this.db.connection.prepare('SELECT * FROM observations WHERE id=?').get(id);
|
|
31
|
+
return row ? ok(map(row)) : missing(`Observation not found: ${id}`);
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
return fail('Failed to read observation', cause);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
update(input) {
|
|
38
|
+
const existing = this.getById(input.id);
|
|
39
|
+
if (!existing.ok)
|
|
40
|
+
return existing;
|
|
41
|
+
const result = this.db.transaction(() => {
|
|
42
|
+
const next = { ...existing.value, ...input, updatedAt: new Date().toISOString() };
|
|
43
|
+
this.db.connection.prepare(`UPDATE observations SET type=@type, title=@title, content=@content, topic_key=@topicKey, updated_at=@updatedAt WHERE id=@id`).run({
|
|
44
|
+
id: input.id,
|
|
45
|
+
type: next.type,
|
|
46
|
+
title: next.title,
|
|
47
|
+
content: next.content,
|
|
48
|
+
topicKey: next.topicKey ?? null,
|
|
49
|
+
updatedAt: next.updatedAt,
|
|
50
|
+
});
|
|
51
|
+
const found = this.getById(input.id);
|
|
52
|
+
if (!found.ok)
|
|
53
|
+
throw new Error(found.error.message);
|
|
54
|
+
return found.value;
|
|
55
|
+
});
|
|
56
|
+
return result.ok ? result : fail(result.error.message, result.error.cause);
|
|
57
|
+
}
|
|
58
|
+
getByTopic(input) {
|
|
59
|
+
try {
|
|
60
|
+
const row = input.topicKey ? this.topic(input) : undefined;
|
|
61
|
+
return row ? ok(map(row)) : missing(`Observation topic not found: ${input.topicKey ?? ''}`);
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
return fail('Failed to read observation by topic', cause);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
search(filters) {
|
|
68
|
+
try {
|
|
69
|
+
const params = { limit: normalizeLimit(filters.limit) };
|
|
70
|
+
const where = [];
|
|
71
|
+
let from = 'observations o';
|
|
72
|
+
if (filters.query?.trim()) {
|
|
73
|
+
from = 'observations o JOIN observations_fts fts ON fts.rowid=o.rowid';
|
|
74
|
+
where.push('observations_fts MATCH @query');
|
|
75
|
+
params.query = toFtsMatchQuery(filters.query);
|
|
76
|
+
}
|
|
77
|
+
for (const [key, column] of [['project', 'project'], ['scope', 'scope'], ['type', 'type'], ['topicKey', 'topic_key']]) {
|
|
78
|
+
const value = filters[key];
|
|
79
|
+
if (value) {
|
|
80
|
+
where.push(`o.${column}=@${key}`);
|
|
81
|
+
params[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const rows = this.db.connection.prepare(`SELECT o.* FROM ${from} ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY o.updated_at DESC LIMIT @limit`).all(params);
|
|
85
|
+
return ok(rows.map(map));
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
return fail('Failed to search observations', cause);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
searchPreviews(filters) {
|
|
92
|
+
const result = this.search(filters);
|
|
93
|
+
if (!result.ok)
|
|
94
|
+
return result;
|
|
95
|
+
return ok(result.value.map(toSearchResult));
|
|
96
|
+
}
|
|
97
|
+
listRevisions(observationId) {
|
|
98
|
+
try {
|
|
99
|
+
const rows = this.db.connection.prepare('SELECT * FROM observation_revisions WHERE observation_id=? ORDER BY revision ASC').all(observationId);
|
|
100
|
+
return ok(rows.map(mapRevision));
|
|
101
|
+
}
|
|
102
|
+
catch (cause) {
|
|
103
|
+
return fail('Failed to read observation revisions', cause);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
topic(input) {
|
|
107
|
+
return this.db.connection.prepare('SELECT * FROM observations WHERE project=@project AND scope=@scope AND type=@type AND topic_key=@topicKey').get(input);
|
|
108
|
+
}
|
|
109
|
+
appendRevision(observationId, input) {
|
|
110
|
+
const current = this.db.connection.prepare('SELECT COALESCE(MAX(revision), 0) AS revision FROM observation_revisions WHERE observation_id=?').get(observationId);
|
|
111
|
+
this.db.connection.prepare(`INSERT INTO observation_revisions(id, observation_id, revision, type, title, content, topic_key, created_at)
|
|
112
|
+
VALUES (@id, @observationId, @revision, @type, @title, @content, @topicKey, @createdAt)`).run({
|
|
113
|
+
id: randomUUID(),
|
|
114
|
+
observationId,
|
|
115
|
+
revision: Number(current?.revision ?? 0) + 1,
|
|
116
|
+
type: input.type,
|
|
117
|
+
title: input.title,
|
|
118
|
+
content: input.content,
|
|
119
|
+
topicKey: input.topicKey ?? null,
|
|
120
|
+
createdAt: input.createdAt,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function map(row) { const value = { id: row.id, project: row.project, scope: row.scope, type: row.type, title: row.title, content: row.content, createdAt: row.created_at, updatedAt: row.updated_at }; if (row.topic_key !== null)
|
|
125
|
+
value.topicKey = row.topic_key; return value; }
|
|
126
|
+
function mapRevision(row) { const value = { id: row.id, observationId: row.observation_id, revision: row.revision, type: row.type, title: row.title, content: row.content, createdAt: row.created_at }; if (row.topic_key !== null)
|
|
127
|
+
value.topicKey = row.topic_key; return value; }
|
|
128
|
+
function toSearchResult(observation) { const value = { id: observation.id, project: observation.project, scope: observation.scope, type: observation.type, title: observation.title, preview: preview(observation.content), createdAt: observation.createdAt, updatedAt: observation.updatedAt }; if (observation.topicKey !== undefined)
|
|
129
|
+
value.topicKey = observation.topicKey; return value; }
|
|
130
|
+
function preview(content) { return content.length <= 160 ? content : `${content.slice(0, 157)}...`; }
|
|
131
|
+
function ok(value) { return { ok: true, value }; }
|
|
132
|
+
function missing(message) { return { ok: false, error: { code: 'not_found', message } }; }
|
|
133
|
+
function fail(message, cause) { return { ok: false, error: { code: 'validation_failed', message, cause } }; }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class SessionRepository {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
save(input) {
|
|
8
|
+
const result = this.db.transaction(() => {
|
|
9
|
+
const id = input.id ?? randomUUID();
|
|
10
|
+
this.db.connection.prepare(`INSERT INTO sessions(id, project, directory, started_at) VALUES (@id, @project, @directory, @startedAt)
|
|
11
|
+
ON CONFLICT(id) DO UPDATE SET project=excluded.project, directory=excluded.directory`).run({ id, project: input.project, directory: input.directory ?? null, startedAt: new Date().toISOString() });
|
|
12
|
+
const session = this.getById(id);
|
|
13
|
+
if (!session.ok)
|
|
14
|
+
throw new Error(session.error.message);
|
|
15
|
+
return session.value;
|
|
16
|
+
});
|
|
17
|
+
return result.ok ? result : fail(result.error.message, result.error.cause);
|
|
18
|
+
}
|
|
19
|
+
getById(id) {
|
|
20
|
+
try {
|
|
21
|
+
const row = this.db.connection.prepare('SELECT * FROM sessions WHERE id=?').get(id);
|
|
22
|
+
if (!row)
|
|
23
|
+
return { ok: false, error: { code: 'not_found', message: `Session not found: ${id}` } };
|
|
24
|
+
return { ok: true, value: mapSession(row) };
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
return fail('Failed to read session', cause);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
restoreLatest(input) {
|
|
31
|
+
try {
|
|
32
|
+
const row = this.db.connection.prepare(`
|
|
33
|
+
SELECT * FROM sessions
|
|
34
|
+
WHERE project = @project
|
|
35
|
+
AND ended_at IS NOT NULL
|
|
36
|
+
AND summary IS NOT NULL
|
|
37
|
+
AND TRIM(summary) <> ''
|
|
38
|
+
AND (@directory IS NULL OR directory = @directory)
|
|
39
|
+
ORDER BY ended_at DESC
|
|
40
|
+
LIMIT 1
|
|
41
|
+
`).get({ project: input.project, directory: input.directory ?? null });
|
|
42
|
+
if (!row)
|
|
43
|
+
return missing(`No closed session summary found for project: ${input.project}`);
|
|
44
|
+
return { ok: true, value: mapSession(row) };
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
return fail('Failed to restore session', cause);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
appendActivity(input) {
|
|
51
|
+
const result = this.db.transaction(() => {
|
|
52
|
+
const next = this.db.connection.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM session_activity WHERE session_id=?').get(input.sessionId);
|
|
53
|
+
const activity = { id: randomUUID(), sequence: next.sequence, createdAt: new Date().toISOString(), ...input };
|
|
54
|
+
this.db.connection.prepare(`INSERT INTO session_activity(id, session_id, sequence, actor, kind, payload_json, created_at)
|
|
55
|
+
VALUES (@id, @sessionId, @sequence, @actor, @kind, @payloadJson, @createdAt)`).run(activity);
|
|
56
|
+
return activity;
|
|
57
|
+
});
|
|
58
|
+
return result.ok ? result : fail(result.error.message, result.error.cause);
|
|
59
|
+
}
|
|
60
|
+
closeSession(input) {
|
|
61
|
+
const summary = input.summary.trim();
|
|
62
|
+
if (summary.length === 0)
|
|
63
|
+
return fail('summary must not be empty');
|
|
64
|
+
const result = this.db.transaction(() => {
|
|
65
|
+
const existing = this.db.connection.prepare('SELECT 1 FROM sessions WHERE id=?').get(input.sessionId);
|
|
66
|
+
if (!existing)
|
|
67
|
+
throw new SessionValidationError(`Session not found: ${input.sessionId}`);
|
|
68
|
+
const endedAt = new Date().toISOString();
|
|
69
|
+
this.db.connection.prepare('UPDATE sessions SET summary=@summary, ended_at=@endedAt WHERE id=@sessionId')
|
|
70
|
+
.run({ sessionId: input.sessionId, summary, endedAt });
|
|
71
|
+
const next = this.db.connection.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM session_activity WHERE session_id=?').get(input.sessionId);
|
|
72
|
+
this.db.connection.prepare(`INSERT INTO session_activity(id, session_id, sequence, actor, kind, payload_json, created_at)
|
|
73
|
+
VALUES (@id, @sessionId, @sequence, @actor, 'summary', @payloadJson, @createdAt)`).run({
|
|
74
|
+
id: randomUUID(),
|
|
75
|
+
sessionId: input.sessionId,
|
|
76
|
+
sequence: next.sequence,
|
|
77
|
+
actor: input.actor,
|
|
78
|
+
payloadJson: JSON.stringify({ summary }),
|
|
79
|
+
createdAt: endedAt,
|
|
80
|
+
});
|
|
81
|
+
const session = this.getById(input.sessionId);
|
|
82
|
+
if (!session.ok)
|
|
83
|
+
throw new SessionValidationError(session.error.message);
|
|
84
|
+
return session.value;
|
|
85
|
+
});
|
|
86
|
+
return result.ok ? result : fail(errorMessage(result.error.cause) ?? result.error.message, result.error.cause);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function fail(message, cause) { return { ok: false, error: { code: 'validation_failed', message, cause } }; }
|
|
90
|
+
function missing(message) { return { ok: false, error: { code: 'not_found', message } }; }
|
|
91
|
+
function mapSession(row) {
|
|
92
|
+
const session = { id: row.id, project: row.project, startedAt: row.started_at };
|
|
93
|
+
if (row.directory !== null)
|
|
94
|
+
session.directory = row.directory;
|
|
95
|
+
if (row.ended_at !== null)
|
|
96
|
+
session.endedAt = row.ended_at;
|
|
97
|
+
if (row.summary !== null)
|
|
98
|
+
session.summary = row.summary;
|
|
99
|
+
return session;
|
|
100
|
+
}
|
|
101
|
+
class SessionValidationError extends Error {
|
|
102
|
+
}
|
|
103
|
+
function errorMessage(cause) {
|
|
104
|
+
return cause instanceof SessionValidationError ? cause.message : undefined;
|
|
105
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class TraceRepository {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
write(input) {
|
|
8
|
+
try {
|
|
9
|
+
const trace = toTrace(input);
|
|
10
|
+
this.db.connection.prepare(`INSERT INTO traces(id, actor, session_id, operation, target_type, target_id, topic_key, status, error_code, error_message, created_at)
|
|
11
|
+
VALUES (@id, @actor, @sessionId, @operation, @targetType, @targetId, @topicKey, @status, @errorCode, @errorMessage, @createdAt)`).run({
|
|
12
|
+
id: trace.id, actor: trace.actor, sessionId: trace.sessionId ?? null, operation: trace.operation, targetType: trace.targetType,
|
|
13
|
+
targetId: trace.targetId ?? null, topicKey: trace.topicKey ?? null, status: trace.status, errorCode: trace.errorCode ?? null, errorMessage: trace.errorMessage ?? null, createdAt: trace.createdAt,
|
|
14
|
+
});
|
|
15
|
+
return { ok: true, value: trace };
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
return fail('Failed to write trace', cause);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
list() {
|
|
22
|
+
try {
|
|
23
|
+
return { ok: true, value: this.db.connection.prepare('SELECT * FROM traces ORDER BY created_at ASC').all().map(map) };
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
return fail('Failed to list traces', cause);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function toTrace(input) {
|
|
31
|
+
const trace = { id: randomUUID(), actor: input.actor, operation: input.operation, targetType: input.targetType, status: input.status, createdAt: new Date().toISOString() };
|
|
32
|
+
if (input.sessionId !== undefined)
|
|
33
|
+
trace.sessionId = input.sessionId;
|
|
34
|
+
if (input.targetId !== undefined)
|
|
35
|
+
trace.targetId = input.targetId;
|
|
36
|
+
if (input.topicKey !== undefined)
|
|
37
|
+
trace.topicKey = input.topicKey;
|
|
38
|
+
if (input.errorCode !== undefined)
|
|
39
|
+
trace.errorCode = input.errorCode;
|
|
40
|
+
if (input.errorMessage !== undefined)
|
|
41
|
+
trace.errorMessage = input.errorMessage;
|
|
42
|
+
return trace;
|
|
43
|
+
}
|
|
44
|
+
function map(row) {
|
|
45
|
+
const trace = { id: row.id, actor: row.actor, operation: row.operation, targetType: row.target_type, status: row.status, createdAt: row.created_at };
|
|
46
|
+
if (row.session_id !== null)
|
|
47
|
+
trace.sessionId = row.session_id;
|
|
48
|
+
if (row.target_id !== null)
|
|
49
|
+
trace.targetId = row.target_id;
|
|
50
|
+
if (row.topic_key !== null)
|
|
51
|
+
trace.topicKey = row.topic_key;
|
|
52
|
+
if (row.error_code !== null)
|
|
53
|
+
trace.errorCode = row.error_code;
|
|
54
|
+
if (row.error_message !== null)
|
|
55
|
+
trace.errorMessage = row.error_message;
|
|
56
|
+
return trace;
|
|
57
|
+
}
|
|
58
|
+
function fail(message, cause) { return { ok: false, error: { code: 'validation_failed', message, cause } }; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function toFtsMatchQuery(query) {
|
|
2
|
+
return query
|
|
3
|
+
.split(/\s+/)
|
|
4
|
+
.map((term) => term.trim())
|
|
5
|
+
.filter(Boolean)
|
|
6
|
+
.map((term) => `"${term.replaceAll('"', '""')}"`)
|
|
7
|
+
.join(' ');
|
|
8
|
+
}
|
|
9
|
+
export function normalizeLimit(limit, fallback = 20, max = 100) {
|
|
10
|
+
return limit === undefined || !Number.isInteger(limit) || limit < 1 ? fallback : Math.min(limit, max);
|
|
11
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const migrationsDirectory = join(dirname(fileURLToPath(import.meta.url)), 'migrations');
|
|
6
|
+
export const sqliteBusyTimeoutMs = 5000;
|
|
7
|
+
export class MemoryDatabase {
|
|
8
|
+
connection;
|
|
9
|
+
constructor(connection) {
|
|
10
|
+
this.connection = connection;
|
|
11
|
+
}
|
|
12
|
+
static open(options) {
|
|
13
|
+
try {
|
|
14
|
+
const connection = new Database(options.path, { readonly: options.readonly ?? false });
|
|
15
|
+
connection.pragma('foreign_keys = ON');
|
|
16
|
+
connection.pragma(`busy_timeout = ${sqliteBusyTimeoutMs}`);
|
|
17
|
+
const memoryDatabase = new MemoryDatabase(connection);
|
|
18
|
+
if (!(options.readonly ?? false)) {
|
|
19
|
+
const migration = memoryDatabase.applyMigrations();
|
|
20
|
+
if (!migration.ok) {
|
|
21
|
+
connection.close();
|
|
22
|
+
return migration;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { ok: true, value: memoryDatabase };
|
|
26
|
+
}
|
|
27
|
+
catch (cause) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: {
|
|
31
|
+
code: 'store_unavailable',
|
|
32
|
+
message: `Unable to open local memory store at ${options.path}`,
|
|
33
|
+
cause,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
applyMigrations() {
|
|
39
|
+
if (!existsSync(migrationsDirectory)) {
|
|
40
|
+
return migrationFailure(`Migrations directory not found: ${migrationsDirectory}`);
|
|
41
|
+
}
|
|
42
|
+
const migrationFiles = readdirSync(migrationsDirectory)
|
|
43
|
+
.filter((file) => file.endsWith('.sql'))
|
|
44
|
+
.sort();
|
|
45
|
+
try {
|
|
46
|
+
const run = this.connection.transaction(() => {
|
|
47
|
+
this.connection.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
49
|
+
version TEXT PRIMARY KEY,
|
|
50
|
+
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
51
|
+
);
|
|
52
|
+
`);
|
|
53
|
+
const hasMigration = this.connection.prepare('SELECT 1 FROM schema_migrations WHERE version = ?');
|
|
54
|
+
const recordMigration = this.connection.prepare('INSERT INTO schema_migrations(version) VALUES (?)');
|
|
55
|
+
for (const file of migrationFiles) {
|
|
56
|
+
if (hasMigration.get(file))
|
|
57
|
+
continue;
|
|
58
|
+
this.connection.exec(readFileSync(join(migrationsDirectory, file), 'utf8'));
|
|
59
|
+
recordMigration.run(file);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
run();
|
|
63
|
+
return { ok: true, value: undefined };
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
return migrationFailure('Failed to apply local memory migrations', cause);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
transaction(operation) {
|
|
70
|
+
try {
|
|
71
|
+
const run = this.connection.transaction(operation);
|
|
72
|
+
return { ok: true, value: run.immediate() };
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: {
|
|
78
|
+
code: 'transaction_failed',
|
|
79
|
+
message: 'Local memory transaction failed and was rolled back',
|
|
80
|
+
cause,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
close() {
|
|
86
|
+
this.connection.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function openMemoryDatabase(options) {
|
|
90
|
+
return MemoryDatabase.open(options);
|
|
91
|
+
}
|
|
92
|
+
function migrationFailure(message, cause) {
|
|
93
|
+
const error = { code: 'migration_failed', message };
|
|
94
|
+
if (cause !== undefined)
|
|
95
|
+
error.cause = cause;
|
|
96
|
+
return { ok: false, error };
|
|
97
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4
|
+
version TEXT PRIMARY KEY,
|
|
5
|
+
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
project TEXT NOT NULL,
|
|
11
|
+
scope TEXT NOT NULL CHECK (scope IN ('project', 'personal')),
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
title TEXT NOT NULL,
|
|
14
|
+
content TEXT NOT NULL,
|
|
15
|
+
topic_key TEXT,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS observations_topic_unique
|
|
21
|
+
ON observations(project, scope, type, topic_key)
|
|
22
|
+
WHERE topic_key IS NOT NULL;
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS observations_project_type_idx
|
|
25
|
+
ON observations(project, type, updated_at DESC);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS observation_revisions (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
observation_id TEXT NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
|
|
30
|
+
revision INTEGER NOT NULL,
|
|
31
|
+
type TEXT NOT NULL,
|
|
32
|
+
title TEXT NOT NULL,
|
|
33
|
+
content TEXT NOT NULL,
|
|
34
|
+
topic_key TEXT,
|
|
35
|
+
created_at TEXT NOT NULL,
|
|
36
|
+
UNIQUE(observation_id, revision)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS observation_revisions_observation_revision_idx
|
|
40
|
+
ON observation_revisions(observation_id, revision DESC);
|
|
41
|
+
|
|
42
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
43
|
+
title,
|
|
44
|
+
content,
|
|
45
|
+
content='observations',
|
|
46
|
+
content_rowid='rowid'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
|
50
|
+
INSERT INTO observations_fts(rowid, title, content)
|
|
51
|
+
VALUES (new.rowid, new.title, new.content);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
|
55
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content)
|
|
56
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
57
|
+
END;
|
|
58
|
+
|
|
59
|
+
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
|
60
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, content)
|
|
61
|
+
VALUES ('delete', old.rowid, old.title, old.content);
|
|
62
|
+
INSERT INTO observations_fts(rowid, title, content)
|
|
63
|
+
VALUES (new.rowid, new.title, new.content);
|
|
64
|
+
END;
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
project TEXT NOT NULL,
|
|
69
|
+
directory TEXT,
|
|
70
|
+
started_at TEXT NOT NULL,
|
|
71
|
+
ended_at TEXT,
|
|
72
|
+
summary TEXT
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS sessions_project_started_idx
|
|
76
|
+
ON sessions(project, started_at DESC);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS session_activity (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
81
|
+
sequence INTEGER NOT NULL,
|
|
82
|
+
actor TEXT NOT NULL,
|
|
83
|
+
kind TEXT NOT NULL CHECK (kind IN ('prompt', 'tool_call', 'artifact', 'summary', 'error')),
|
|
84
|
+
payload_json TEXT NOT NULL,
|
|
85
|
+
created_at TEXT NOT NULL,
|
|
86
|
+
UNIQUE(session_id, sequence)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS session_activity_session_sequence_idx
|
|
90
|
+
ON session_activity(session_id, sequence);
|
|
91
|
+
|
|
92
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
project TEXT NOT NULL,
|
|
95
|
+
topic_key TEXT NOT NULL,
|
|
96
|
+
phase TEXT NOT NULL,
|
|
97
|
+
content TEXT NOT NULL,
|
|
98
|
+
observation_id TEXT NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
|
|
99
|
+
created_at TEXT NOT NULL,
|
|
100
|
+
updated_at TEXT NOT NULL,
|
|
101
|
+
UNIQUE(project, topic_key)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS artifacts_project_phase_idx
|
|
105
|
+
ON artifacts(project, phase, updated_at DESC);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
actor TEXT NOT NULL,
|
|
110
|
+
session_id TEXT,
|
|
111
|
+
operation TEXT NOT NULL,
|
|
112
|
+
target_type TEXT NOT NULL,
|
|
113
|
+
target_id TEXT,
|
|
114
|
+
topic_key TEXT,
|
|
115
|
+
status TEXT NOT NULL CHECK (status IN ('ok', 'error')),
|
|
116
|
+
error_code TEXT,
|
|
117
|
+
error_message TEXT,
|
|
118
|
+
created_at TEXT NOT NULL
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS traces_operation_created_idx
|
|
122
|
+
ON traces(operation, created_at DESC);
|
|
123
|
+
|
|
124
|
+
CREATE INDEX IF NOT EXISTS traces_target_idx
|
|
125
|
+
ON traces(target_type, target_id);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS traces_session_idx
|
|
128
|
+
ON traces(session_id, created_at DESC);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS observation_revisions (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
observation_id TEXT NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
|
|
4
|
+
revision INTEGER NOT NULL,
|
|
5
|
+
type TEXT NOT NULL,
|
|
6
|
+
title TEXT NOT NULL,
|
|
7
|
+
content TEXT NOT NULL,
|
|
8
|
+
topic_key TEXT,
|
|
9
|
+
created_at TEXT NOT NULL,
|
|
10
|
+
UNIQUE(observation_id, revision)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS observation_revisions_observation_revision_idx
|
|
14
|
+
ON observation_revisions(observation_id, revision DESC);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
project TEXT NOT NULL,
|
|
4
|
+
scope TEXT NOT NULL CHECK (scope IN ('project', 'personal')),
|
|
5
|
+
mode TEXT NOT NULL CHECK (mode IN ('agent', 'subagent')),
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
description TEXT NOT NULL,
|
|
8
|
+
instructions_json TEXT NOT NULL,
|
|
9
|
+
capabilities_json TEXT NOT NULL,
|
|
10
|
+
permissions_json TEXT NOT NULL,
|
|
11
|
+
memory_json TEXT NOT NULL,
|
|
12
|
+
workflows_json TEXT NOT NULL,
|
|
13
|
+
skills_json TEXT NOT NULL,
|
|
14
|
+
adapters_json TEXT NOT NULL,
|
|
15
|
+
parent_agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
updated_at TEXT NOT NULL,
|
|
18
|
+
UNIQUE(project, scope, name),
|
|
19
|
+
CHECK ((mode = 'agent' AND parent_agent_id IS NULL) OR (mode = 'subagent' AND parent_agent_id IS NOT NULL))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS agents_project_scope_mode_idx
|
|
23
|
+
ON agents(project, scope, mode, name);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS agents_parent_idx
|
|
26
|
+
ON agents(parent_agent_id, name);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
project TEXT NOT NULL,
|
|
4
|
+
user_intent TEXT NOT NULL,
|
|
5
|
+
workflow TEXT NOT NULL,
|
|
6
|
+
phase TEXT NOT NULL,
|
|
7
|
+
selected_agent_id TEXT NOT NULL,
|
|
8
|
+
provider_adapter TEXT NOT NULL,
|
|
9
|
+
model TEXT NOT NULL,
|
|
10
|
+
status TEXT NOT NULL CHECK (status IN ('created', 'planned', 'running', 'needs-human', 'completed', 'failed', 'blocked', 'cancelled')),
|
|
11
|
+
outcome TEXT CHECK (outcome IN ('success', 'partial', 'failure', 'blocked', 'cancelled')),
|
|
12
|
+
outcome_reason TEXT,
|
|
13
|
+
latest_checkpoint_id TEXT REFERENCES run_checkpoints(id) ON DELETE SET NULL,
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
updated_at TEXT NOT NULL,
|
|
16
|
+
completed_at TEXT,
|
|
17
|
+
CHECK (
|
|
18
|
+
(status IN ('created', 'planned', 'running', 'needs-human') AND outcome IS NULL AND completed_at IS NULL)
|
|
19
|
+
OR (status = 'completed' AND outcome IN ('success', 'partial') AND completed_at IS NOT NULL)
|
|
20
|
+
OR (status = 'failed' AND outcome = 'failure' AND completed_at IS NOT NULL)
|
|
21
|
+
OR (status = 'blocked' AND outcome = 'blocked' AND completed_at IS NOT NULL)
|
|
22
|
+
OR (status = 'cancelled' AND outcome = 'cancelled' AND completed_at IS NOT NULL)
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS runs_project_created_idx
|
|
27
|
+
ON runs(project, created_at DESC);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS runs_status_updated_idx
|
|
30
|
+
ON runs(status, updated_at DESC);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
35
|
+
sequence INTEGER NOT NULL,
|
|
36
|
+
kind TEXT NOT NULL CHECK (kind IN ('timeline', 'evidence', 'memory-operation', 'artifact-reference', 'tool-call', 'permission-decision', 'operation-execution', 'approval', 'verification')),
|
|
37
|
+
title TEXT NOT NULL,
|
|
38
|
+
payload_json TEXT NOT NULL,
|
|
39
|
+
related_type TEXT,
|
|
40
|
+
related_id TEXT,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
UNIQUE(run_id, sequence)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS run_events_run_sequence_idx
|
|
46
|
+
ON run_events(run_id, sequence);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS run_events_related_idx
|
|
49
|
+
ON run_events(related_type, related_id);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS run_checkpoints (
|
|
52
|
+
id TEXT PRIMARY KEY,
|
|
53
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
54
|
+
sequence INTEGER NOT NULL,
|
|
55
|
+
label TEXT NOT NULL,
|
|
56
|
+
state_json TEXT NOT NULL,
|
|
57
|
+
created_at TEXT NOT NULL,
|
|
58
|
+
UNIQUE(run_id, sequence)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS run_checkpoints_run_sequence_idx
|
|
62
|
+
ON run_checkpoints(run_id, sequence);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS run_approvals (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
4
|
+
decision_event_id TEXT NOT NULL REFERENCES run_events(id) ON DELETE CASCADE,
|
|
5
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
|
|
6
|
+
requested_at TEXT NOT NULL,
|
|
7
|
+
resolved_at TEXT,
|
|
8
|
+
resolved_by TEXT,
|
|
9
|
+
resolution_reason TEXT,
|
|
10
|
+
CHECK (
|
|
11
|
+
(status = 'pending' AND resolved_at IS NULL AND resolved_by IS NULL AND resolution_reason IS NULL)
|
|
12
|
+
OR (status IN ('approved', 'rejected', 'cancelled') AND resolved_at IS NOT NULL AND resolved_by IS NOT NULL)
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE UNIQUE INDEX IF NOT EXISTS run_approvals_decision_event_idx
|
|
17
|
+
ON run_approvals(decision_event_id);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS run_approvals_run_status_idx
|
|
20
|
+
ON run_approvals(run_id, status, requested_at DESC);
|