@exaudeus/workrail 0.1.0 → 0.1.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 (30) hide show
  1. package/README.md +153 -189
  2. package/dist/application/services/classification-engine.d.ts +33 -0
  3. package/dist/application/services/classification-engine.js +258 -0
  4. package/dist/application/services/compression-service.d.ts +20 -0
  5. package/dist/application/services/compression-service.js +312 -0
  6. package/dist/application/services/context-management-service.d.ts +38 -0
  7. package/dist/application/services/context-management-service.js +301 -0
  8. package/dist/application/services/context-persistence-service.d.ts +45 -0
  9. package/dist/application/services/context-persistence-service.js +273 -0
  10. package/dist/cli/migrate-workflow.js +3 -2
  11. package/dist/infrastructure/storage/context-storage.d.ts +150 -0
  12. package/dist/infrastructure/storage/context-storage.js +40 -0
  13. package/dist/infrastructure/storage/filesystem-blob-storage.d.ts +27 -0
  14. package/dist/infrastructure/storage/filesystem-blob-storage.js +363 -0
  15. package/dist/infrastructure/storage/hybrid-context-storage.d.ts +29 -0
  16. package/dist/infrastructure/storage/hybrid-context-storage.js +400 -0
  17. package/dist/infrastructure/storage/migrations/001_initial_schema.sql +38 -0
  18. package/dist/infrastructure/storage/migrations/002_context_concurrency_enhancements.sql +234 -0
  19. package/dist/infrastructure/storage/migrations/003_classification_overrides.sql +20 -0
  20. package/dist/infrastructure/storage/sqlite-metadata-storage.d.ts +35 -0
  21. package/dist/infrastructure/storage/sqlite-metadata-storage.js +410 -0
  22. package/dist/infrastructure/storage/sqlite-migrator.d.ts +46 -0
  23. package/dist/infrastructure/storage/sqlite-migrator.js +293 -0
  24. package/dist/types/context-types.d.ts +236 -0
  25. package/dist/types/context-types.js +10 -0
  26. package/dist/utils/storage-security.js +1 -1
  27. package/package.json +4 -1
  28. package/workflows/coding-task-workflow-with-loops.json +434 -0
  29. package/workflows/mr-review-workflow.json +75 -26
  30. package/workflows/systemic-bug-investigation-with-loops.json +423 -0
@@ -0,0 +1,35 @@
1
+ import { IMetadataStorage, ContextStorageConfig, MetadataStats } from './context-storage';
2
+ import { CheckpointMetadata, SessionInfo } from '../../types/context-types';
3
+ export declare class SqliteMetadataStorage implements IMetadataStorage {
4
+ private db;
5
+ private migrator;
6
+ private config;
7
+ private isInitialized;
8
+ private heartbeatIntervals;
9
+ private cleanupInterval;
10
+ private statements;
11
+ constructor(config: ContextStorageConfig);
12
+ initialize(): Promise<void>;
13
+ saveCheckpointMetadata(metadata: CheckpointMetadata): Promise<void>;
14
+ getCheckpointMetadata(checkpointId: string): Promise<CheckpointMetadata | null>;
15
+ listCheckpointMetadata(sessionId: string, limit?: number, offset?: number): Promise<CheckpointMetadata[]>;
16
+ deleteCheckpointMetadata(checkpointId: string): Promise<void>;
17
+ upsertSessionInfo(session: SessionInfo): Promise<void>;
18
+ getSessionInfo(sessionId: string): Promise<SessionInfo | null>;
19
+ deleteSessionInfo(sessionId: string): Promise<void>;
20
+ acquireSessionLock(sessionId: string, operationType: string, timeoutMs?: number): Promise<string>;
21
+ releaseSessionLock(operationId: string): Promise<void>;
22
+ updateOperationHeartbeat(operationId: string): Promise<void>;
23
+ cleanupStaleOperations(): Promise<number>;
24
+ getMetadataStats(): Promise<MetadataStats>;
25
+ close(): Promise<void>;
26
+ private ensureInitialized;
27
+ private ensureDataDirectory;
28
+ private prepareStatements;
29
+ private mapRowToCheckpointMetadata;
30
+ private cleanupInterruptedOperations;
31
+ private startHeartbeat;
32
+ private stopHeartbeat;
33
+ private setupAutomaticCleanup;
34
+ private cleanup;
35
+ }
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SqliteMetadataStorage = void 0;
7
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const sqlite_migrator_1 = require("./sqlite-migrator");
12
+ class SqliteMetadataStorage {
13
+ constructor(config) {
14
+ this.db = null;
15
+ this.migrator = null;
16
+ this.isInitialized = false;
17
+ this.heartbeatIntervals = new Map();
18
+ this.cleanupInterval = null;
19
+ this.statements = {};
20
+ this.config = config;
21
+ }
22
+ async initialize() {
23
+ if (this.isInitialized) {
24
+ return;
25
+ }
26
+ try {
27
+ await this.ensureDataDirectory();
28
+ const dbPath = path_1.default.join(this.config.dataDirectory, this.config.database.path);
29
+ this.db = new better_sqlite3_1.default(dbPath, {
30
+ timeout: this.config.database.timeout,
31
+ verbose: undefined
32
+ });
33
+ if (this.config.database.walMode) {
34
+ this.db.pragma('journal_mode = WAL');
35
+ }
36
+ this.db.pragma('synchronous = NORMAL');
37
+ this.db.pragma('cache_size = 1000');
38
+ this.db.pragma('temp_store = memory');
39
+ this.db.pragma('foreign_keys = ON');
40
+ this.migrator = new sqlite_migrator_1.SqliteMigrator(dbPath);
41
+ await this.migrator.initialize();
42
+ const migrationResults = await this.migrator.migrate();
43
+ if (migrationResults.some(result => !result.success)) {
44
+ throw new Error('Database migration failed');
45
+ }
46
+ const isValid = await this.migrator.validateIntegrity();
47
+ if (!isValid) {
48
+ throw new Error('Database integrity validation failed');
49
+ }
50
+ await this.prepareStatements();
51
+ await this.cleanupInterruptedOperations();
52
+ this.setupAutomaticCleanup();
53
+ this.isInitialized = true;
54
+ console.log('✅ SQLite metadata storage initialized');
55
+ }
56
+ catch (error) {
57
+ await this.cleanup();
58
+ throw new Error(`Failed to initialize SQLite metadata storage: ${error}`);
59
+ }
60
+ }
61
+ async saveCheckpointMetadata(metadata) {
62
+ this.ensureInitialized();
63
+ try {
64
+ this.statements.insertCheckpoint.run(metadata.id, metadata.sessionId, metadata.name || null, metadata.agentId || null, metadata.createdAt, metadata.tags ? JSON.stringify(metadata.tags) : null, metadata.contextSizeBytes, metadata.contextHash, metadata.blobPath, metadata.status, null, 1.0, metadata.classificationSummary ? JSON.stringify(metadata.classificationSummary) : null);
65
+ console.log(`📦 Saved checkpoint metadata: ${metadata.id}`);
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Failed to save checkpoint metadata: ${error}`);
69
+ }
70
+ }
71
+ async getCheckpointMetadata(checkpointId) {
72
+ this.ensureInitialized();
73
+ try {
74
+ const row = this.statements.selectCheckpoint.get(checkpointId);
75
+ if (!row) {
76
+ return null;
77
+ }
78
+ return this.mapRowToCheckpointMetadata(row);
79
+ }
80
+ catch (error) {
81
+ throw new Error(`Failed to get checkpoint metadata: ${error}`);
82
+ }
83
+ }
84
+ async listCheckpointMetadata(sessionId, limit = 20, offset = 0) {
85
+ this.ensureInitialized();
86
+ try {
87
+ const rows = this.statements.selectCheckpointsBySession.all(sessionId, limit, offset);
88
+ return rows.map(row => this.mapRowToCheckpointMetadata(row));
89
+ }
90
+ catch (error) {
91
+ throw new Error(`Failed to list checkpoint metadata: ${error}`);
92
+ }
93
+ }
94
+ async deleteCheckpointMetadata(checkpointId) {
95
+ this.ensureInitialized();
96
+ try {
97
+ const result = this.statements.deleteCheckpoint.run(checkpointId);
98
+ if (result.changes === 0) {
99
+ throw new Error(`Checkpoint ${checkpointId} not found`);
100
+ }
101
+ console.log(`🗑️ Deleted checkpoint metadata: ${checkpointId}`);
102
+ }
103
+ catch (error) {
104
+ throw new Error(`Failed to delete checkpoint metadata: ${error}`);
105
+ }
106
+ }
107
+ async upsertSessionInfo(session) {
108
+ this.ensureInitialized();
109
+ try {
110
+ this.statements.upsertSession.run(session.id, session.createdAt, session.lastAccessedAt, session.tags ? JSON.stringify(session.tags) : null, session.totalSizeBytes);
111
+ console.log(`📋 Upserted session: ${session.id}`);
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Failed to upsert session: ${error}`);
115
+ }
116
+ }
117
+ async getSessionInfo(sessionId) {
118
+ this.ensureInitialized();
119
+ try {
120
+ const row = this.statements.selectSession.get(sessionId);
121
+ if (!row) {
122
+ return null;
123
+ }
124
+ return {
125
+ id: row.id,
126
+ createdAt: row.created_at,
127
+ lastAccessedAt: row.last_accessed_at,
128
+ tags: row.tags ? JSON.parse(row.tags) : undefined,
129
+ totalSizeBytes: row.total_size_bytes
130
+ };
131
+ }
132
+ catch (error) {
133
+ throw new Error(`Failed to get session info: ${error}`);
134
+ }
135
+ }
136
+ async deleteSessionInfo(sessionId) {
137
+ this.ensureInitialized();
138
+ try {
139
+ const result = this.statements.deleteSession.run(sessionId);
140
+ if (result.changes === 0) {
141
+ throw new Error(`Session ${sessionId} not found`);
142
+ }
143
+ console.log(`🗑️ Deleted session: ${sessionId}`);
144
+ }
145
+ catch (error) {
146
+ throw new Error(`Failed to delete session: ${error}`);
147
+ }
148
+ }
149
+ async acquireSessionLock(sessionId, operationType, timeoutMs) {
150
+ this.ensureInitialized();
151
+ const operationId = crypto_1.default.randomUUID();
152
+ const timeout = timeoutMs || this.config.concurrency.operationTimeoutMs;
153
+ const expiresAt = new Date(Date.now() + timeout).toISOString();
154
+ const maxRetries = this.config.concurrency.maxRetries;
155
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
156
+ try {
157
+ await this.cleanupStaleOperations();
158
+ this.statements.insertActiveOperation.run(operationId, sessionId, operationType, new Date().toISOString(), new Date().toISOString(), timeout, JSON.stringify({ attempt, maxRetries }));
159
+ this.statements.acquireSessionLock.run(sessionId, operationId, new Date().toISOString(), expiresAt);
160
+ this.startHeartbeat(operationId);
161
+ console.log(`🔒 Acquired lock for session ${sessionId}, operation: ${operationId}`);
162
+ return operationId;
163
+ }
164
+ catch (error) {
165
+ if (attempt === maxRetries) {
166
+ throw new Error(`Failed to acquire session lock for ${sessionId} after ${maxRetries} attempts: ${error}`);
167
+ }
168
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
169
+ await new Promise(resolve => setTimeout(resolve, delay));
170
+ }
171
+ }
172
+ throw new Error(`Unable to acquire session lock for ${sessionId}`);
173
+ }
174
+ async releaseSessionLock(operationId) {
175
+ this.ensureInitialized();
176
+ try {
177
+ this.stopHeartbeat(operationId);
178
+ this.statements.releaseSessionLock.run(operationId);
179
+ this.statements.deleteActiveOperation.run(operationId);
180
+ console.log(`🔓 Released lock for operation: ${operationId}`);
181
+ }
182
+ catch (error) {
183
+ console.warn(`Warning: Failed to release session lock ${operationId}:`, error);
184
+ }
185
+ }
186
+ async updateOperationHeartbeat(operationId) {
187
+ this.ensureInitialized();
188
+ try {
189
+ this.statements.updateHeartbeat.run(new Date().toISOString(), operationId);
190
+ }
191
+ catch (error) {
192
+ console.warn(`Warning: Failed to update heartbeat for ${operationId}:`, error);
193
+ }
194
+ }
195
+ async cleanupStaleOperations() {
196
+ this.ensureInitialized();
197
+ try {
198
+ const result = this.statements.cleanupStaleOperations.run();
199
+ if (result.changes > 0) {
200
+ console.log(`🧹 Cleaned up ${result.changes} stale operations`);
201
+ }
202
+ return result.changes;
203
+ }
204
+ catch (error) {
205
+ console.warn('Warning: Failed to cleanup stale operations:', error);
206
+ return 0;
207
+ }
208
+ }
209
+ async getMetadataStats() {
210
+ this.ensureInitialized();
211
+ try {
212
+ const countsQuery = `
213
+ SELECT
214
+ (SELECT COUNT(*) FROM sessions) as sessions,
215
+ (SELECT COUNT(*) FROM checkpoint_metadata) as checkpoints,
216
+ (SELECT COUNT(*) FROM active_operations) as active_ops,
217
+ (SELECT AVG(context_size_bytes) FROM checkpoint_metadata) as avg_size
218
+ `;
219
+ const counts = this.db.prepare(countsQuery).get();
220
+ const dbPath = path_1.default.join(this.config.dataDirectory, this.config.database.path);
221
+ const dbStats = fs_1.default.statSync(dbPath);
222
+ return {
223
+ databaseSizeBytes: dbStats.size,
224
+ totalSessions: counts.sessions || 0,
225
+ totalCheckpoints: counts.checkpoints || 0,
226
+ activeOperations: counts.active_ops || 0,
227
+ averageCheckpointSize: counts.avg_size || 0,
228
+ indexEfficiency: 1.0
229
+ };
230
+ }
231
+ catch (error) {
232
+ throw new Error(`Failed to get metadata stats: ${error}`);
233
+ }
234
+ }
235
+ async close() {
236
+ await this.cleanup();
237
+ }
238
+ ensureInitialized() {
239
+ if (!this.isInitialized || !this.db) {
240
+ throw new Error('SqliteMetadataStorage not initialized');
241
+ }
242
+ }
243
+ async ensureDataDirectory() {
244
+ try {
245
+ await fs_1.default.promises.mkdir(this.config.dataDirectory, {
246
+ recursive: true,
247
+ mode: this.config.blobs.directoryPermissions
248
+ });
249
+ }
250
+ catch (error) {
251
+ throw new Error(`Failed to create data directory: ${error}`);
252
+ }
253
+ }
254
+ async prepareStatements() {
255
+ if (!this.db)
256
+ return;
257
+ this.statements = {
258
+ insertCheckpoint: this.db.prepare(`
259
+ INSERT OR REPLACE INTO checkpoint_metadata (
260
+ id, session_id, name, agent_id, created_at, tags,
261
+ context_size_bytes, context_hash, blob_path, status,
262
+ created_by_operation, compression_ratio, classification_info
263
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
264
+ `),
265
+ selectCheckpoint: this.db.prepare(`
266
+ SELECT * FROM checkpoint_metadata WHERE id = ?
267
+ `),
268
+ selectCheckpointsBySession: this.db.prepare(`
269
+ SELECT * FROM checkpoint_metadata
270
+ WHERE session_id = ?
271
+ ORDER BY created_at DESC
272
+ LIMIT ? OFFSET ?
273
+ `),
274
+ deleteCheckpoint: this.db.prepare(`
275
+ DELETE FROM checkpoint_metadata WHERE id = ?
276
+ `),
277
+ upsertSession: this.db.prepare(`
278
+ INSERT OR REPLACE INTO sessions (
279
+ id, created_at, last_accessed_at, tags, total_size_bytes
280
+ ) VALUES (?, ?, ?, ?, ?)
281
+ `),
282
+ selectSession: this.db.prepare(`
283
+ SELECT * FROM sessions WHERE id = ?
284
+ `),
285
+ deleteSession: this.db.prepare(`
286
+ DELETE FROM sessions WHERE id = ?
287
+ `),
288
+ insertActiveOperation: this.db.prepare(`
289
+ INSERT INTO active_operations (
290
+ id, session_id, operation_type, started_at, heartbeat_at, timeout_ms, metadata
291
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
292
+ `),
293
+ deleteActiveOperation: this.db.prepare(`
294
+ DELETE FROM active_operations WHERE id = ?
295
+ `),
296
+ updateHeartbeat: this.db.prepare(`
297
+ UPDATE active_operations SET heartbeat_at = ? WHERE id = ?
298
+ `),
299
+ cleanupStaleOperations: this.db.prepare(`
300
+ DELETE FROM active_operations
301
+ WHERE heartbeat_at < datetime('now', '-5 minutes')
302
+ OR started_at < datetime('now', '-10 minutes')
303
+ `),
304
+ acquireSessionLock: this.db.prepare(`
305
+ UPDATE sessions
306
+ SET locked_at = ?, locked_by = ?, lock_timeout_at = ?
307
+ WHERE id = ? AND (locked_at IS NULL OR lock_timeout_at < CURRENT_TIMESTAMP)
308
+ `),
309
+ releaseSessionLock: this.db.prepare(`
310
+ UPDATE sessions
311
+ SET locked_at = NULL, locked_by = NULL, lock_timeout_at = NULL
312
+ WHERE locked_by = ?
313
+ `),
314
+ checkSessionLock: this.db.prepare(`
315
+ SELECT locked_by, lock_timeout_at FROM sessions
316
+ WHERE id = ? AND locked_at IS NOT NULL
317
+ `)
318
+ };
319
+ }
320
+ mapRowToCheckpointMetadata(row) {
321
+ return {
322
+ id: row.id,
323
+ sessionId: row.session_id,
324
+ name: row.name || undefined,
325
+ agentId: row.agent_id || undefined,
326
+ createdAt: row.created_at,
327
+ tags: row.tags ? JSON.parse(row.tags) : undefined,
328
+ contextSizeBytes: row.context_size_bytes,
329
+ contextHash: row.context_hash,
330
+ blobPath: row.blob_path,
331
+ status: row.status,
332
+ classificationSummary: row.classification_info ? JSON.parse(row.classification_info) : undefined
333
+ };
334
+ }
335
+ async cleanupInterruptedOperations() {
336
+ try {
337
+ const cleanupCount = await this.cleanupStaleOperations();
338
+ if (cleanupCount > 0) {
339
+ console.log(`🔄 Cleaned up ${cleanupCount} interrupted operations from previous session`);
340
+ }
341
+ this.db.prepare(`
342
+ UPDATE sessions
343
+ SET locked_at = NULL, locked_by = NULL, lock_timeout_at = NULL
344
+ WHERE lock_timeout_at < CURRENT_TIMESTAMP
345
+ `).run();
346
+ }
347
+ catch (error) {
348
+ console.warn('Warning: Failed to cleanup interrupted operations:', error);
349
+ }
350
+ }
351
+ startHeartbeat(operationId) {
352
+ const interval = setInterval(async () => {
353
+ try {
354
+ await this.updateOperationHeartbeat(operationId);
355
+ }
356
+ catch (error) {
357
+ console.warn(`Heartbeat failed for operation ${operationId}:`, error);
358
+ this.stopHeartbeat(operationId);
359
+ }
360
+ }, this.config.concurrency.heartbeatIntervalMs);
361
+ this.heartbeatIntervals.set(operationId, interval);
362
+ }
363
+ stopHeartbeat(operationId) {
364
+ const interval = this.heartbeatIntervals.get(operationId);
365
+ if (interval) {
366
+ clearInterval(interval);
367
+ this.heartbeatIntervals.delete(operationId);
368
+ }
369
+ }
370
+ setupAutomaticCleanup() {
371
+ this.cleanupInterval = setInterval(async () => {
372
+ try {
373
+ await this.cleanupStaleOperations();
374
+ }
375
+ catch (error) {
376
+ console.warn('Automatic cleanup failed:', error);
377
+ }
378
+ }, this.config.concurrency.cleanupIntervalMs);
379
+ process.on('exit', () => {
380
+ if (this.cleanupInterval) {
381
+ clearInterval(this.cleanupInterval);
382
+ }
383
+ });
384
+ }
385
+ async cleanup() {
386
+ try {
387
+ for (const interval of this.heartbeatIntervals.values()) {
388
+ clearInterval(interval);
389
+ }
390
+ this.heartbeatIntervals.clear();
391
+ if (this.cleanupInterval) {
392
+ clearInterval(this.cleanupInterval);
393
+ this.cleanupInterval = null;
394
+ }
395
+ if (this.migrator) {
396
+ this.migrator.close();
397
+ this.migrator = null;
398
+ }
399
+ if (this.db) {
400
+ this.db.close();
401
+ this.db = null;
402
+ }
403
+ this.isInitialized = false;
404
+ }
405
+ catch (error) {
406
+ console.warn('Warning during SQLite metadata storage cleanup:', error);
407
+ }
408
+ }
409
+ }
410
+ exports.SqliteMetadataStorage = SqliteMetadataStorage;
@@ -0,0 +1,46 @@
1
+ export interface Migration {
2
+ version: number;
3
+ filename: string;
4
+ description: string;
5
+ sql: string;
6
+ checksum: string;
7
+ }
8
+ export interface MigrationResult {
9
+ success: boolean;
10
+ appliedVersion?: number;
11
+ error?: string;
12
+ rollbackPerformed?: boolean;
13
+ duration?: number;
14
+ }
15
+ export interface MigrationStatus {
16
+ currentVersion: number;
17
+ targetVersion: number;
18
+ pendingMigrations: Migration[];
19
+ appliedMigrations: AppliedMigration[];
20
+ }
21
+ export interface AppliedMigration {
22
+ version: number;
23
+ description: string;
24
+ appliedAt: string;
25
+ checksum: string;
26
+ }
27
+ export declare class SqliteMigrator {
28
+ private db;
29
+ private migrationsDir;
30
+ private readonly maxRetries;
31
+ private readonly lockTimeout;
32
+ constructor(databasePath: string, migrationsDir?: string);
33
+ initialize(): Promise<void>;
34
+ getCurrentVersion(): number;
35
+ loadAvailableMigrations(): Migration[];
36
+ getMigrationStatus(): MigrationStatus;
37
+ migrate(): Promise<MigrationResult[]>;
38
+ private applyMigration;
39
+ private acquireMigrationLock;
40
+ private releaseMigrationLock;
41
+ private validateMigrationChecksum;
42
+ private calculateChecksum;
43
+ validateIntegrity(): Promise<boolean>;
44
+ close(): void;
45
+ getStats(): Record<string, any>;
46
+ }