scene-capability-engine 3.6.3 → 3.6.4
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/CHANGELOG.md +33 -0
- package/README.md +8 -0
- package/README.zh.md +8 -0
- package/bin/scene-capability-engine.js +31 -1
- package/docs/command-reference.md +48 -0
- package/lib/collab/agent-registry.js +38 -1
- package/lib/commands/session.js +60 -2
- package/lib/commands/state.js +210 -0
- package/lib/runtime/project-timeline.js +202 -17
- package/lib/runtime/session-store.js +167 -14
- package/lib/state/sce-state-store.js +629 -0
- package/lib/state/state-migration-manager.js +659 -0
- package/lib/steering/compliance-error-reporter.js +6 -0
- package/lib/steering/steering-compliance-checker.js +43 -8
- package/package.json +2 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const { spawnSync } = require('child_process');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const minimatchModule = require('minimatch');
|
|
5
|
+
const { getSceStateStore } = require('../state/sce-state-store');
|
|
5
6
|
|
|
6
7
|
const minimatch = typeof minimatchModule === 'function'
|
|
7
8
|
? minimatchModule
|
|
@@ -86,13 +87,25 @@ function createSnapshotId(prefix = 'ts') {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
class ProjectTimelineStore {
|
|
89
|
-
constructor(projectPath = process.cwd(), fileSystem = fs) {
|
|
90
|
+
constructor(projectPath = process.cwd(), fileSystem = fs, options = {}) {
|
|
90
91
|
this._projectPath = projectPath;
|
|
91
92
|
this._fileSystem = fileSystem;
|
|
92
93
|
this._timelineDir = path.join(projectPath, TIMELINE_DIR);
|
|
93
94
|
this._indexPath = path.join(this._timelineDir, TIMELINE_INDEX_FILE);
|
|
94
95
|
this._snapshotsDir = path.join(this._timelineDir, TIMELINE_SNAPSHOTS_DIR);
|
|
95
96
|
this._configPath = path.join(projectPath, TIMELINE_CONFIG_RELATIVE_PATH);
|
|
97
|
+
this._env = options.env || process.env;
|
|
98
|
+
this._stateStore = options.stateStore || getSceStateStore(projectPath, {
|
|
99
|
+
fileSystem: this._fileSystem,
|
|
100
|
+
env: this._env,
|
|
101
|
+
sqliteModule: options.sqliteModule
|
|
102
|
+
});
|
|
103
|
+
this._preferSqliteReads = normalizeBoolean(
|
|
104
|
+
options.preferSqliteReads !== undefined
|
|
105
|
+
? options.preferSqliteReads
|
|
106
|
+
: (this._env && this._env.SCE_TIMELINE_PREFER_SQLITE_READS),
|
|
107
|
+
true
|
|
108
|
+
);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
async getConfig() {
|
|
@@ -166,8 +179,10 @@ class ProjectTimelineStore {
|
|
|
166
179
|
};
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
const
|
|
170
|
-
const latest =
|
|
182
|
+
const readResult = await this._readTimelineEntriesForRead({ limit: 1 });
|
|
183
|
+
const latest = Array.isArray(readResult.entries) && readResult.entries.length > 0
|
|
184
|
+
? readResult.entries[0]
|
|
185
|
+
: null;
|
|
171
186
|
const intervalMinutes = normalizePositiveInteger(options.intervalMinutes, config.auto_interval_minutes, 24 * 60);
|
|
172
187
|
|
|
173
188
|
if (latest && latest.created_at) {
|
|
@@ -287,6 +302,7 @@ class ProjectTimelineStore {
|
|
|
287
302
|
}
|
|
288
303
|
|
|
289
304
|
await this._writeIndex(index);
|
|
305
|
+
await this._syncTimelineIndexEntries([entry], 'runtime.project-timeline.save');
|
|
290
306
|
|
|
291
307
|
return {
|
|
292
308
|
...entry,
|
|
@@ -296,21 +312,21 @@ class ProjectTimelineStore {
|
|
|
296
312
|
}
|
|
297
313
|
|
|
298
314
|
async listSnapshots(options = {}) {
|
|
299
|
-
const index = await this._readIndex();
|
|
300
315
|
const trigger = `${options.trigger || ''}`.trim();
|
|
301
316
|
const limit = normalizePositiveInteger(options.limit, 20, 1000);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
snapshots = snapshots.slice(0, limit);
|
|
317
|
+
const readResult = await this._readTimelineEntriesForRead({
|
|
318
|
+
trigger,
|
|
319
|
+
limit
|
|
320
|
+
});
|
|
321
|
+
const consistency = await this._getTimelineIndexConsistency();
|
|
308
322
|
|
|
309
323
|
return {
|
|
310
324
|
mode: 'timeline-list',
|
|
311
325
|
success: true,
|
|
312
|
-
total:
|
|
313
|
-
|
|
326
|
+
total: readResult.entries.length,
|
|
327
|
+
read_source: readResult.source,
|
|
328
|
+
consistency,
|
|
329
|
+
snapshots: readResult.entries
|
|
314
330
|
};
|
|
315
331
|
}
|
|
316
332
|
|
|
@@ -320,8 +336,8 @@ class ProjectTimelineStore {
|
|
|
320
336
|
throw new Error('snapshotId is required');
|
|
321
337
|
}
|
|
322
338
|
|
|
323
|
-
const
|
|
324
|
-
const entry =
|
|
339
|
+
const resolved = await this._resolveTimelineEntryBySnapshotIdWithSource(normalizedId);
|
|
340
|
+
const entry = resolved.entry;
|
|
325
341
|
if (!entry) {
|
|
326
342
|
throw new Error(`Timeline snapshot not found: ${normalizedId}`);
|
|
327
343
|
}
|
|
@@ -346,6 +362,8 @@ class ProjectTimelineStore {
|
|
|
346
362
|
return {
|
|
347
363
|
mode: 'timeline-show',
|
|
348
364
|
success: true,
|
|
365
|
+
read_source: resolved.source,
|
|
366
|
+
consistency: await this._getTimelineIndexConsistency(),
|
|
349
367
|
snapshot: entry,
|
|
350
368
|
metadata,
|
|
351
369
|
files
|
|
@@ -359,8 +377,7 @@ class ProjectTimelineStore {
|
|
|
359
377
|
}
|
|
360
378
|
|
|
361
379
|
const config = await this.getConfig();
|
|
362
|
-
const
|
|
363
|
-
const entry = index.snapshots.find((item) => item.snapshot_id === normalizedId);
|
|
380
|
+
const entry = await this._resolveTimelineEntryBySnapshotId(normalizedId);
|
|
364
381
|
if (!entry) {
|
|
365
382
|
throw new Error(`Timeline snapshot not found: ${normalizedId}`);
|
|
366
383
|
}
|
|
@@ -416,6 +433,174 @@ class ProjectTimelineStore {
|
|
|
416
433
|
};
|
|
417
434
|
}
|
|
418
435
|
|
|
436
|
+
_normalizeTimelineIndexedEntry(row = {}) {
|
|
437
|
+
return {
|
|
438
|
+
snapshot_id: `${row.snapshot_id || ''}`.trim(),
|
|
439
|
+
created_at: `${row.created_at || ''}`.trim(),
|
|
440
|
+
trigger: `${row.trigger || ''}`.trim(),
|
|
441
|
+
event: `${row.event || ''}`.trim(),
|
|
442
|
+
summary: `${row.summary || ''}`.trim(),
|
|
443
|
+
scene_id: row.scene_id ? `${row.scene_id}`.trim() : null,
|
|
444
|
+
session_id: row.session_id ? `${row.session_id}`.trim() : null,
|
|
445
|
+
command: row.command ? `${row.command}`.trim() : null,
|
|
446
|
+
file_count: Number.isFinite(Number(row.file_count)) ? Number(row.file_count) : 0,
|
|
447
|
+
total_bytes: Number.isFinite(Number(row.total_bytes)) ? Number(row.total_bytes) : 0,
|
|
448
|
+
path: `${row.snapshot_path || row.path || ''}`.trim(),
|
|
449
|
+
git: row && typeof row.git === 'object' ? row.git : {}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async _readTimelineEntriesFromSqlite(options = {}) {
|
|
454
|
+
if (!this._stateStore || this._preferSqliteReads !== true) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
const rows = await this._stateStore.listTimelineSnapshotIndex({
|
|
459
|
+
trigger: options.trigger,
|
|
460
|
+
snapshotId: options.snapshotId,
|
|
461
|
+
limit: options.limit
|
|
462
|
+
});
|
|
463
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
return rows.map((row) => this._normalizeTimelineIndexedEntry(row));
|
|
467
|
+
} catch (_error) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async _readTimelineEntriesFromFile(options = {}) {
|
|
473
|
+
const index = await this._readIndex();
|
|
474
|
+
const trigger = `${options.trigger || ''}`.trim();
|
|
475
|
+
const snapshotId = safeSnapshotId(options.snapshotId);
|
|
476
|
+
const limit = normalizePositiveInteger(options.limit, 20, 1000);
|
|
477
|
+
|
|
478
|
+
let snapshots = Array.isArray(index.snapshots) ? [...index.snapshots] : [];
|
|
479
|
+
if (trigger) {
|
|
480
|
+
snapshots = snapshots.filter((item) => `${item.trigger || ''}`.trim() === trigger);
|
|
481
|
+
}
|
|
482
|
+
if (snapshotId) {
|
|
483
|
+
snapshots = snapshots.filter((item) => `${item.snapshot_id || ''}`.trim() === snapshotId);
|
|
484
|
+
}
|
|
485
|
+
if (limit > 0) {
|
|
486
|
+
snapshots = snapshots.slice(0, limit);
|
|
487
|
+
}
|
|
488
|
+
return snapshots.map((item) => this._normalizeTimelineIndexedEntry(item));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async _readTimelineEntriesForRead(options = {}) {
|
|
492
|
+
const sqliteEntries = await this._readTimelineEntriesFromSqlite(options);
|
|
493
|
+
if (Array.isArray(sqliteEntries) && sqliteEntries.length > 0) {
|
|
494
|
+
return {
|
|
495
|
+
source: 'sqlite',
|
|
496
|
+
entries: sqliteEntries
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const fileEntries = await this._readTimelineEntriesFromFile(options);
|
|
500
|
+
return {
|
|
501
|
+
source: 'file',
|
|
502
|
+
entries: fileEntries
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async _resolveTimelineEntryBySnapshotId(snapshotId) {
|
|
507
|
+
const resolved = await this._resolveTimelineEntryBySnapshotIdWithSource(snapshotId);
|
|
508
|
+
return resolved.entry;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async _resolveTimelineEntryBySnapshotIdWithSource(snapshotId) {
|
|
512
|
+
const normalizedId = safeSnapshotId(snapshotId);
|
|
513
|
+
if (!normalizedId) {
|
|
514
|
+
return {
|
|
515
|
+
source: 'file',
|
|
516
|
+
entry: null
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const fromSqlite = await this._readTimelineEntriesFromSqlite({
|
|
520
|
+
snapshotId: normalizedId,
|
|
521
|
+
limit: 1
|
|
522
|
+
});
|
|
523
|
+
if (Array.isArray(fromSqlite) && fromSqlite.length > 0) {
|
|
524
|
+
return {
|
|
525
|
+
source: 'sqlite',
|
|
526
|
+
entry: fromSqlite[0]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const fromFile = await this._readTimelineEntriesFromFile({
|
|
530
|
+
snapshotId: normalizedId,
|
|
531
|
+
limit: 1
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
source: 'file',
|
|
535
|
+
entry: Array.isArray(fromFile) && fromFile.length > 0 ? fromFile[0] : null
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async _syncTimelineIndexEntries(entries = [], source = 'runtime.project-timeline') {
|
|
540
|
+
if (!this._stateStore || !Array.isArray(entries) || entries.length === 0) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
await this._stateStore.upsertTimelineSnapshotIndex(entries.map((item) => ({
|
|
545
|
+
snapshot_id: item.snapshot_id,
|
|
546
|
+
created_at: item.created_at,
|
|
547
|
+
trigger: item.trigger,
|
|
548
|
+
event: item.event,
|
|
549
|
+
summary: item.summary,
|
|
550
|
+
scene_id: item.scene_id,
|
|
551
|
+
session_id: item.session_id,
|
|
552
|
+
command: item.command,
|
|
553
|
+
file_count: item.file_count,
|
|
554
|
+
total_bytes: item.total_bytes,
|
|
555
|
+
snapshot_path: item.path,
|
|
556
|
+
git: item.git || {}
|
|
557
|
+
})), {
|
|
558
|
+
source
|
|
559
|
+
});
|
|
560
|
+
} catch (_error) {
|
|
561
|
+
// best effort sync, keep file index as source of truth
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async _getTimelineIndexConsistency() {
|
|
566
|
+
const index = await this._readIndex();
|
|
567
|
+
const fileCount = Array.isArray(index.snapshots) ? index.snapshots.length : 0;
|
|
568
|
+
let sqliteCount = null;
|
|
569
|
+
if (this._stateStore) {
|
|
570
|
+
try {
|
|
571
|
+
const rows = await this._stateStore.listTimelineSnapshotIndex({ limit: 0 });
|
|
572
|
+
if (Array.isArray(rows)) {
|
|
573
|
+
sqliteCount = rows.length;
|
|
574
|
+
}
|
|
575
|
+
} catch (_error) {
|
|
576
|
+
sqliteCount = null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let status = 'file-only';
|
|
581
|
+
if (sqliteCount === null) {
|
|
582
|
+
status = 'sqlite-unavailable';
|
|
583
|
+
} else if (fileCount === 0 && sqliteCount === 0) {
|
|
584
|
+
status = 'empty';
|
|
585
|
+
} else if (fileCount === 0 && sqliteCount > 0) {
|
|
586
|
+
status = 'sqlite-only';
|
|
587
|
+
} else if (fileCount > 0 && sqliteCount === 0) {
|
|
588
|
+
status = 'file-only';
|
|
589
|
+
} else if (fileCount === sqliteCount) {
|
|
590
|
+
status = 'aligned';
|
|
591
|
+
} else if (sqliteCount < fileCount) {
|
|
592
|
+
status = 'pending-sync';
|
|
593
|
+
} else if (sqliteCount > fileCount) {
|
|
594
|
+
status = 'sqlite-ahead';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
file_index_count: fileCount,
|
|
599
|
+
sqlite_index_count: sqliteCount,
|
|
600
|
+
status
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
419
604
|
_buildExcludePatterns(configPatterns = []) {
|
|
420
605
|
const defaults = DEFAULT_TIMELINE_CONFIG.exclude_paths;
|
|
421
606
|
return Array.from(new Set([
|
|
@@ -560,7 +745,7 @@ class ProjectTimelineStore {
|
|
|
560
745
|
|
|
561
746
|
async function captureTimelineCheckpoint(options = {}, dependencies = {}) {
|
|
562
747
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
563
|
-
const store = dependencies.timelineStore || new ProjectTimelineStore(projectPath, dependencies.fileSystem || fs);
|
|
748
|
+
const store = dependencies.timelineStore || new ProjectTimelineStore(projectPath, dependencies.fileSystem || fs, dependencies);
|
|
564
749
|
|
|
565
750
|
try {
|
|
566
751
|
if (options.auto !== false) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { SteeringContract, normalizeToolName } = require('./steering-contract');
|
|
4
|
+
const { getSceStateStore } = require('../state/sce-state-store');
|
|
4
5
|
|
|
5
6
|
const SESSION_SCHEMA_VERSION = '1.0';
|
|
6
7
|
const SESSION_DIR = path.join('.sce', 'sessions');
|
|
@@ -43,12 +44,22 @@ function nextSnapshotId(session) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
class SessionStore {
|
|
46
|
-
constructor(workspaceRoot, steeringContract = null) {
|
|
47
|
+
constructor(workspaceRoot, steeringContract = null, options = {}) {
|
|
47
48
|
this._workspaceRoot = workspaceRoot;
|
|
48
49
|
this._sessionsDir = path.join(workspaceRoot, SESSION_DIR);
|
|
49
50
|
this._sessionGovernanceDir = path.join(workspaceRoot, SESSION_GOVERNANCE_DIR);
|
|
50
51
|
this._sceneIndexPath = path.join(this._sessionGovernanceDir, SESSION_SCENE_INDEX_FILE);
|
|
51
52
|
this._steeringContract = steeringContract || new SteeringContract(workspaceRoot);
|
|
53
|
+
this._fileSystem = options.fileSystem || fs;
|
|
54
|
+
this._env = options.env || process.env;
|
|
55
|
+
this._stateStore = options.stateStore || getSceStateStore(workspaceRoot, {
|
|
56
|
+
fileSystem: this._fileSystem,
|
|
57
|
+
env: this._env,
|
|
58
|
+
sqliteModule: options.sqliteModule
|
|
59
|
+
});
|
|
60
|
+
this._preferSqliteSceneReads = options.preferSqliteSceneReads !== undefined
|
|
61
|
+
? options.preferSqliteSceneReads === true
|
|
62
|
+
: true;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
async startSession(options = {}) {
|
|
@@ -61,10 +72,10 @@ class SessionStore {
|
|
|
61
72
|
|
|
62
73
|
await this._steeringContract.ensureContract();
|
|
63
74
|
const steeringPayload = await this._steeringContract.buildCompilePayload(tool, agentVersion);
|
|
64
|
-
await
|
|
75
|
+
await this._fileSystem.ensureDir(this._sessionsDir);
|
|
65
76
|
|
|
66
77
|
const sessionPath = this._sessionPath(sessionId);
|
|
67
|
-
if (await
|
|
78
|
+
if (await this._fileSystem.pathExists(sessionPath)) {
|
|
68
79
|
throw new Error(`Session already exists: ${sessionId}`);
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -161,10 +172,10 @@ class SessionStore {
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
async listSessions() {
|
|
164
|
-
if (!await
|
|
175
|
+
if (!await this._fileSystem.pathExists(this._sessionsDir)) {
|
|
165
176
|
return [];
|
|
166
177
|
}
|
|
167
|
-
const entries = await
|
|
178
|
+
const entries = await this._fileSystem.readdir(this._sessionsDir);
|
|
168
179
|
const records = [];
|
|
169
180
|
for (const entry of entries) {
|
|
170
181
|
if (!entry.endsWith('.json')) {
|
|
@@ -172,7 +183,7 @@ class SessionStore {
|
|
|
172
183
|
}
|
|
173
184
|
const sessionId = entry.slice(0, -'.json'.length);
|
|
174
185
|
try {
|
|
175
|
-
const session = await
|
|
186
|
+
const session = await this._fileSystem.readJson(this._sessionPath(sessionId));
|
|
176
187
|
records.push(session);
|
|
177
188
|
} catch (_error) {
|
|
178
189
|
// Ignore unreadable entries.
|
|
@@ -542,10 +553,47 @@ class SessionStore {
|
|
|
542
553
|
}
|
|
543
554
|
|
|
544
555
|
async listSceneRecords() {
|
|
556
|
+
const sqliteRecords = await this._readSceneRecordsFromSqlite();
|
|
557
|
+
if (Array.isArray(sqliteRecords) && sqliteRecords.length > 0) {
|
|
558
|
+
return sqliteRecords;
|
|
559
|
+
}
|
|
545
560
|
const sceneIndex = await this._readSceneIndex();
|
|
546
561
|
return Object.values(sceneIndex.scenes || {});
|
|
547
562
|
}
|
|
548
563
|
|
|
564
|
+
async getSceneIndexDiagnostics() {
|
|
565
|
+
const sceneIndex = await this._readSceneIndex();
|
|
566
|
+
const fileRecords = Object.values(sceneIndex.scenes || {});
|
|
567
|
+
const sqliteRecords = await this._readSceneRecordsFromSqlite();
|
|
568
|
+
|
|
569
|
+
const fileCount = fileRecords.length;
|
|
570
|
+
const sqliteCount = Array.isArray(sqliteRecords) ? sqliteRecords.length : null;
|
|
571
|
+
|
|
572
|
+
let status = 'file-only';
|
|
573
|
+
if (sqliteCount === null) {
|
|
574
|
+
status = 'sqlite-unavailable';
|
|
575
|
+
} else if (fileCount === 0 && sqliteCount === 0) {
|
|
576
|
+
status = 'empty';
|
|
577
|
+
} else if (fileCount === 0 && sqliteCount > 0) {
|
|
578
|
+
status = 'sqlite-only';
|
|
579
|
+
} else if (fileCount > 0 && sqliteCount === 0) {
|
|
580
|
+
status = 'file-only';
|
|
581
|
+
} else if (fileCount === sqliteCount) {
|
|
582
|
+
status = 'aligned';
|
|
583
|
+
} else if (sqliteCount < fileCount) {
|
|
584
|
+
status = 'pending-sync';
|
|
585
|
+
} else if (sqliteCount > fileCount) {
|
|
586
|
+
status = 'sqlite-ahead';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
read_preference: this._preferSqliteSceneReads ? 'sqlite' : 'file',
|
|
591
|
+
file_scene_count: fileCount,
|
|
592
|
+
sqlite_scene_count: sqliteCount,
|
|
593
|
+
status
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
549
597
|
async listActiveSceneSessions() {
|
|
550
598
|
const records = await this.listSceneRecords();
|
|
551
599
|
const active = [];
|
|
@@ -610,20 +658,20 @@ class SessionStore {
|
|
|
610
658
|
throw new Error(`Invalid session id: ${ref}`);
|
|
611
659
|
}
|
|
612
660
|
const sessionPath = this._sessionPath(sessionId);
|
|
613
|
-
if (!await
|
|
661
|
+
if (!await this._fileSystem.pathExists(sessionPath)) {
|
|
614
662
|
throw new Error(`Session not found: ${sessionId}`);
|
|
615
663
|
}
|
|
616
|
-
const session = await
|
|
664
|
+
const session = await this._fileSystem.readJson(sessionPath);
|
|
617
665
|
return { sessionId, session };
|
|
618
666
|
}
|
|
619
667
|
|
|
620
668
|
async _writeSession(sessionId, session) {
|
|
621
|
-
await
|
|
622
|
-
await
|
|
669
|
+
await this._fileSystem.ensureDir(this._sessionsDir);
|
|
670
|
+
await this._fileSystem.writeJson(this._sessionPath(sessionId), session, { spaces: 2 });
|
|
623
671
|
}
|
|
624
672
|
|
|
625
673
|
async _readSceneIndex() {
|
|
626
|
-
if (!await
|
|
674
|
+
if (!await this._fileSystem.pathExists(this._sceneIndexPath)) {
|
|
627
675
|
return {
|
|
628
676
|
schema_version: SESSION_GOVERNANCE_SCHEMA_VERSION,
|
|
629
677
|
updated_at: nowIso(),
|
|
@@ -632,7 +680,7 @@ class SessionStore {
|
|
|
632
680
|
}
|
|
633
681
|
|
|
634
682
|
try {
|
|
635
|
-
const payload = await
|
|
683
|
+
const payload = await this._fileSystem.readJson(this._sceneIndexPath);
|
|
636
684
|
return {
|
|
637
685
|
schema_version: payload && payload.schema_version
|
|
638
686
|
? payload.schema_version
|
|
@@ -659,11 +707,116 @@ class SessionStore {
|
|
|
659
707
|
? indexPayload.scenes
|
|
660
708
|
: {}
|
|
661
709
|
};
|
|
662
|
-
await
|
|
663
|
-
await
|
|
710
|
+
await this._fileSystem.ensureDir(this._sessionGovernanceDir);
|
|
711
|
+
await this._fileSystem.writeJson(this._sceneIndexPath, payload, { spaces: 2 });
|
|
712
|
+
await this._syncSceneIndexToStateStore(payload);
|
|
664
713
|
return payload;
|
|
665
714
|
}
|
|
666
715
|
|
|
716
|
+
async _readSceneRecordsFromSqlite() {
|
|
717
|
+
if (this._preferSqliteSceneReads !== true || !this._stateStore) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const cycles = await this._stateStore.listSceneSessionCycles({ limit: 0 });
|
|
722
|
+
if (!Array.isArray(cycles) || cycles.length === 0) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const grouped = new Map();
|
|
726
|
+
for (const cycle of cycles) {
|
|
727
|
+
const sceneId = `${cycle.scene_id || ''}`.trim();
|
|
728
|
+
if (!sceneId) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!grouped.has(sceneId)) {
|
|
732
|
+
grouped.set(sceneId, []);
|
|
733
|
+
}
|
|
734
|
+
grouped.get(sceneId).push({
|
|
735
|
+
cycle: Number(cycle.cycle || 0),
|
|
736
|
+
session_id: `${cycle.session_id || ''}`.trim(),
|
|
737
|
+
status: `${cycle.status || ''}`.trim() || null,
|
|
738
|
+
started_at: cycle.started_at || null,
|
|
739
|
+
completed_at: cycle.completed_at || null
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const sceneRecords = [];
|
|
744
|
+
for (const [sceneId, rows] of grouped.entries()) {
|
|
745
|
+
rows.sort((left, right) => right.cycle - left.cycle);
|
|
746
|
+
const active = rows.find((item) => item.status === 'active') || null;
|
|
747
|
+
const completed = rows.find((item) => item.status === 'completed') || null;
|
|
748
|
+
const lastCycle = rows.length > 0 ? rows[0].cycle : 0;
|
|
749
|
+
|
|
750
|
+
sceneRecords.push({
|
|
751
|
+
scene_id: sceneId,
|
|
752
|
+
active_session_id: active ? active.session_id : null,
|
|
753
|
+
active_cycle: active ? active.cycle : null,
|
|
754
|
+
latest_completed_session_id: completed ? completed.session_id : null,
|
|
755
|
+
last_cycle: lastCycle,
|
|
756
|
+
cycles: rows.map((item) => ({
|
|
757
|
+
cycle: item.cycle,
|
|
758
|
+
session_id: item.session_id,
|
|
759
|
+
status: item.status,
|
|
760
|
+
started_at: item.started_at,
|
|
761
|
+
completed_at: item.completed_at
|
|
762
|
+
}))
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
sceneRecords.sort((left, right) => `${left.scene_id}`.localeCompare(`${right.scene_id}`));
|
|
767
|
+
return sceneRecords;
|
|
768
|
+
} catch (_error) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
_flattenSceneCycleRows(indexPayload = {}) {
|
|
774
|
+
const scenes = indexPayload && typeof indexPayload.scenes === 'object' && indexPayload.scenes
|
|
775
|
+
? indexPayload.scenes
|
|
776
|
+
: {};
|
|
777
|
+
const records = [];
|
|
778
|
+
for (const [sceneId, sceneRecord] of Object.entries(scenes)) {
|
|
779
|
+
const normalizedSceneId = `${sceneId || ''}`.trim();
|
|
780
|
+
if (!normalizedSceneId) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const cycles = Array.isArray(sceneRecord && sceneRecord.cycles) ? sceneRecord.cycles : [];
|
|
784
|
+
for (const cycle of cycles) {
|
|
785
|
+
const cycleNo = Number.parseInt(`${cycle && cycle.cycle}`, 10);
|
|
786
|
+
const sessionId = `${cycle && cycle.session_id ? cycle.session_id : ''}`.trim();
|
|
787
|
+
if (!Number.isFinite(cycleNo) || cycleNo <= 0 || !sessionId) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
records.push({
|
|
791
|
+
scene_id: normalizedSceneId,
|
|
792
|
+
cycle: cycleNo,
|
|
793
|
+
session_id: sessionId,
|
|
794
|
+
status: `${cycle && cycle.status ? cycle.status : ''}`.trim() || null,
|
|
795
|
+
started_at: cycle && cycle.started_at ? cycle.started_at : null,
|
|
796
|
+
completed_at: cycle && cycle.completed_at ? cycle.completed_at : null
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return records;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async _syncSceneIndexToStateStore(indexPayload = {}) {
|
|
804
|
+
if (!this._stateStore) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const records = this._flattenSceneCycleRows(indexPayload);
|
|
809
|
+
if (records.length === 0) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
await this._stateStore.upsertSceneSessionCycles(records, {
|
|
813
|
+
source: 'runtime.session-store'
|
|
814
|
+
});
|
|
815
|
+
} catch (_error) {
|
|
816
|
+
// best effort sync only
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
667
820
|
_sessionPath(sessionId) {
|
|
668
821
|
return path.join(this._sessionsDir, `${sessionId}.json`);
|
|
669
822
|
}
|