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.
@@ -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 index = await this._readIndex();
170
- const latest = index.snapshots[0] || null;
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
- let snapshots = Array.isArray(index.snapshots) ? [...index.snapshots] : [];
304
- if (trigger) {
305
- snapshots = snapshots.filter((item) => `${item.trigger || ''}`.trim() === trigger);
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: snapshots.length,
313
- snapshots
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 index = await this._readIndex();
324
- const entry = index.snapshots.find((item) => item.snapshot_id === normalizedId);
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 index = await this._readIndex();
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 fs.ensureDir(this._sessionsDir);
75
+ await this._fileSystem.ensureDir(this._sessionsDir);
65
76
 
66
77
  const sessionPath = this._sessionPath(sessionId);
67
- if (await fs.pathExists(sessionPath)) {
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 fs.pathExists(this._sessionsDir)) {
175
+ if (!await this._fileSystem.pathExists(this._sessionsDir)) {
165
176
  return [];
166
177
  }
167
- const entries = await fs.readdir(this._sessionsDir);
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 fs.readJson(this._sessionPath(sessionId));
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 fs.pathExists(sessionPath)) {
661
+ if (!await this._fileSystem.pathExists(sessionPath)) {
614
662
  throw new Error(`Session not found: ${sessionId}`);
615
663
  }
616
- const session = await fs.readJson(sessionPath);
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 fs.ensureDir(this._sessionsDir);
622
- await fs.writeJson(this._sessionPath(sessionId), session, { spaces: 2 });
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 fs.pathExists(this._sceneIndexPath)) {
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 fs.readJson(this._sceneIndexPath);
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 fs.ensureDir(this._sessionGovernanceDir);
663
- await fs.writeJson(this._sceneIndexPath, payload, { spaces: 2 });
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
  }