scene-capability-engine 3.6.2 → 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.
@@ -1,4 +1,5 @@
1
- const path = require('path');
1
+ const path = require('path');
2
+ const crypto = require('crypto');
2
3
  const fs = require('fs-extra');
3
4
 
4
5
  const DEFAULT_BACKEND = 'sqlite';
@@ -20,6 +21,14 @@ function normalizeInteger(value, fallback = 0) {
20
21
  return parsed;
21
22
  }
22
23
 
24
+ function normalizeNonNegativeInteger(value, fallback = 0) {
25
+ const parsed = Number.parseInt(`${value}`, 10);
26
+ if (!Number.isFinite(parsed) || parsed < 0) {
27
+ return fallback;
28
+ }
29
+ return parsed;
30
+ }
31
+
23
32
  function parseJsonSafe(value, fallback) {
24
33
  if (typeof value !== 'string' || !value.trim()) {
25
34
  return fallback;
@@ -31,6 +40,27 @@ function parseJsonSafe(value, fallback) {
31
40
  }
32
41
  }
33
42
 
43
+ function normalizeStringArray(value, fallback = []) {
44
+ if (!Array.isArray(value)) {
45
+ return [...fallback];
46
+ }
47
+ return value
48
+ .map((item) => normalizeString(item))
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function normalizeIsoTimestamp(value, fallback = '') {
53
+ const normalized = normalizeString(value);
54
+ if (!normalized) {
55
+ return normalizeString(fallback);
56
+ }
57
+ const parsed = Date.parse(normalized);
58
+ if (!Number.isFinite(parsed)) {
59
+ return normalizeString(fallback);
60
+ }
61
+ return new Date(parsed).toISOString();
62
+ }
63
+
34
64
  function formatSegment(value) {
35
65
  const normalized = normalizeInteger(value, 0);
36
66
  if (normalized <= 0) {
@@ -83,6 +113,12 @@ class SceStateStore {
83
113
  specs: {},
84
114
  tasks: {},
85
115
  refs: {},
116
+ timeline_snapshots: {},
117
+ scene_session_cycles: {},
118
+ agent_runtime: {},
119
+ migration_records: {},
120
+ auth_leases: {},
121
+ auth_events: [],
86
122
  sequences: {
87
123
  scene_next: 1,
88
124
  spec_next_by_scene: {},
@@ -181,6 +217,106 @@ class SceStateStore {
181
217
 
182
218
  CREATE INDEX IF NOT EXISTS idx_studio_event_stream_job_ts
183
219
  ON studio_event_stream(job_id, event_timestamp);
220
+
221
+ CREATE TABLE IF NOT EXISTS auth_lease_registry (
222
+ lease_id TEXT PRIMARY KEY,
223
+ subject TEXT NOT NULL,
224
+ role TEXT NOT NULL,
225
+ scope_json TEXT NOT NULL,
226
+ reason TEXT,
227
+ metadata_json TEXT,
228
+ issued_at TEXT NOT NULL,
229
+ expires_at TEXT NOT NULL,
230
+ revoked_at TEXT,
231
+ created_at TEXT NOT NULL,
232
+ updated_at TEXT NOT NULL
233
+ );
234
+
235
+ CREATE INDEX IF NOT EXISTS idx_auth_lease_registry_expires
236
+ ON auth_lease_registry(expires_at);
237
+
238
+ CREATE TABLE IF NOT EXISTS auth_event_stream (
239
+ event_id TEXT PRIMARY KEY,
240
+ event_timestamp TEXT NOT NULL,
241
+ event_type TEXT NOT NULL,
242
+ action TEXT,
243
+ actor TEXT,
244
+ lease_id TEXT,
245
+ result TEXT,
246
+ target TEXT,
247
+ detail_json TEXT,
248
+ created_at TEXT NOT NULL
249
+ );
250
+
251
+ CREATE INDEX IF NOT EXISTS idx_auth_event_stream_ts
252
+ ON auth_event_stream(event_timestamp);
253
+
254
+ CREATE TABLE IF NOT EXISTS timeline_snapshot_registry (
255
+ snapshot_id TEXT PRIMARY KEY,
256
+ created_at TEXT NOT NULL,
257
+ trigger TEXT,
258
+ event TEXT,
259
+ summary TEXT,
260
+ scene_id TEXT,
261
+ session_id TEXT,
262
+ command TEXT,
263
+ file_count INTEGER,
264
+ total_bytes INTEGER,
265
+ snapshot_path TEXT,
266
+ git_json TEXT,
267
+ source TEXT,
268
+ updated_at TEXT NOT NULL
269
+ );
270
+
271
+ CREATE INDEX IF NOT EXISTS idx_timeline_snapshot_registry_created
272
+ ON timeline_snapshot_registry(created_at DESC);
273
+
274
+ CREATE TABLE IF NOT EXISTS scene_session_cycle_registry (
275
+ scene_id TEXT NOT NULL,
276
+ cycle INTEGER NOT NULL,
277
+ session_id TEXT NOT NULL,
278
+ status TEXT,
279
+ started_at TEXT,
280
+ completed_at TEXT,
281
+ source TEXT,
282
+ updated_at TEXT NOT NULL,
283
+ PRIMARY KEY (scene_id, cycle)
284
+ );
285
+
286
+ CREATE INDEX IF NOT EXISTS idx_scene_session_cycle_registry_session
287
+ ON scene_session_cycle_registry(session_id);
288
+
289
+ CREATE TABLE IF NOT EXISTS agent_runtime_registry (
290
+ agent_id TEXT PRIMARY KEY,
291
+ machine_id TEXT,
292
+ instance_index INTEGER,
293
+ hostname TEXT,
294
+ registered_at TEXT,
295
+ last_heartbeat TEXT,
296
+ status TEXT,
297
+ current_task_json TEXT,
298
+ source TEXT,
299
+ updated_at TEXT NOT NULL
300
+ );
301
+
302
+ CREATE INDEX IF NOT EXISTS idx_agent_runtime_registry_status
303
+ ON agent_runtime_registry(status);
304
+
305
+ CREATE TABLE IF NOT EXISTS state_migration_registry (
306
+ migration_id TEXT PRIMARY KEY,
307
+ component_id TEXT NOT NULL,
308
+ source_path TEXT,
309
+ mode TEXT NOT NULL,
310
+ status TEXT NOT NULL,
311
+ metrics_json TEXT,
312
+ detail_json TEXT,
313
+ started_at TEXT NOT NULL,
314
+ completed_at TEXT,
315
+ updated_at TEXT NOT NULL
316
+ );
317
+
318
+ CREATE INDEX IF NOT EXISTS idx_state_migration_registry_component_started
319
+ ON state_migration_registry(component_id, started_at DESC);
184
320
  `);
185
321
  }
186
322
 
@@ -264,6 +400,117 @@ class SceStateStore {
264
400
  };
265
401
  }
266
402
 
403
+ _mapAuthLeaseRow(row) {
404
+ if (!row) {
405
+ return null;
406
+ }
407
+ return {
408
+ lease_id: normalizeString(row.lease_id),
409
+ subject: normalizeString(row.subject),
410
+ role: normalizeString(row.role),
411
+ scope: normalizeStringArray(parseJsonSafe(row.scope_json, []), ['project:*']),
412
+ reason: normalizeString(row.reason) || null,
413
+ metadata: parseJsonSafe(row.metadata_json, {}) || {},
414
+ issued_at: normalizeIsoTimestamp(row.issued_at) || null,
415
+ expires_at: normalizeIsoTimestamp(row.expires_at) || null,
416
+ revoked_at: normalizeIsoTimestamp(row.revoked_at) || null,
417
+ created_at: normalizeIsoTimestamp(row.created_at) || null,
418
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
419
+ };
420
+ }
421
+
422
+ _mapAuthEventRow(row) {
423
+ if (!row) {
424
+ return null;
425
+ }
426
+ return {
427
+ event_id: normalizeString(row.event_id),
428
+ event_timestamp: normalizeIsoTimestamp(row.event_timestamp) || null,
429
+ event_type: normalizeString(row.event_type),
430
+ action: normalizeString(row.action) || null,
431
+ actor: normalizeString(row.actor) || null,
432
+ lease_id: normalizeString(row.lease_id) || null,
433
+ result: normalizeString(row.result) || null,
434
+ target: normalizeString(row.target) || null,
435
+ detail: parseJsonSafe(row.detail_json, {}) || {},
436
+ created_at: normalizeIsoTimestamp(row.created_at) || null
437
+ };
438
+ }
439
+
440
+ _mapTimelineSnapshotRow(row) {
441
+ if (!row) {
442
+ return null;
443
+ }
444
+ return {
445
+ snapshot_id: normalizeString(row.snapshot_id),
446
+ created_at: normalizeIsoTimestamp(row.created_at) || null,
447
+ trigger: normalizeString(row.trigger) || null,
448
+ event: normalizeString(row.event) || null,
449
+ summary: normalizeString(row.summary) || null,
450
+ scene_id: normalizeString(row.scene_id) || null,
451
+ session_id: normalizeString(row.session_id) || null,
452
+ command: normalizeString(row.command) || null,
453
+ file_count: normalizeNonNegativeInteger(row.file_count, 0),
454
+ total_bytes: normalizeNonNegativeInteger(row.total_bytes, 0),
455
+ snapshot_path: normalizeString(row.snapshot_path) || null,
456
+ git: parseJsonSafe(row.git_json, {}) || {},
457
+ source: normalizeString(row.source) || null,
458
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
459
+ };
460
+ }
461
+
462
+ _mapSceneSessionCycleRow(row) {
463
+ if (!row) {
464
+ return null;
465
+ }
466
+ return {
467
+ scene_id: normalizeString(row.scene_id),
468
+ cycle: normalizeNonNegativeInteger(row.cycle, 0),
469
+ session_id: normalizeString(row.session_id),
470
+ status: normalizeString(row.status) || null,
471
+ started_at: normalizeIsoTimestamp(row.started_at) || null,
472
+ completed_at: normalizeIsoTimestamp(row.completed_at) || null,
473
+ source: normalizeString(row.source) || null,
474
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
475
+ };
476
+ }
477
+
478
+ _mapAgentRuntimeRow(row) {
479
+ if (!row) {
480
+ return null;
481
+ }
482
+ return {
483
+ agent_id: normalizeString(row.agent_id),
484
+ machine_id: normalizeString(row.machine_id) || null,
485
+ instance_index: normalizeNonNegativeInteger(row.instance_index, 0),
486
+ hostname: normalizeString(row.hostname) || null,
487
+ registered_at: normalizeIsoTimestamp(row.registered_at) || null,
488
+ last_heartbeat: normalizeIsoTimestamp(row.last_heartbeat) || null,
489
+ status: normalizeString(row.status) || null,
490
+ current_task: parseJsonSafe(row.current_task_json, null),
491
+ source: normalizeString(row.source) || null,
492
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
493
+ };
494
+ }
495
+
496
+ _mapStateMigrationRow(row) {
497
+ if (!row) {
498
+ return null;
499
+ }
500
+ return {
501
+ migration_id: normalizeString(row.migration_id),
502
+ component_id: normalizeString(row.component_id),
503
+ source_path: normalizeString(row.source_path) || null,
504
+ mode: normalizeString(row.mode) || null,
505
+ status: normalizeString(row.status) || null,
506
+ metrics: parseJsonSafe(row.metrics_json, {}) || {},
507
+ detail: parseJsonSafe(row.detail_json, {}) || {},
508
+ started_at: normalizeIsoTimestamp(row.started_at) || null,
509
+ completed_at: normalizeIsoTimestamp(row.completed_at) || null,
510
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
511
+ };
512
+ }
513
+
267
514
  async resolveOrCreateTaskRef(options = {}) {
268
515
  const sceneId = normalizeString(options.sceneId);
269
516
  const specId = normalizeString(options.specId);
@@ -510,6 +757,763 @@ class SceStateStore {
510
757
  return events;
511
758
  }
512
759
 
760
+ async issueAuthLease(options = {}) {
761
+ const subject = normalizeString(options.subject) || 'unknown';
762
+ const role = normalizeString(options.role) || 'maintainer';
763
+ const scope = normalizeStringArray(options.scope, ['project:*']);
764
+ const reason = normalizeString(options.reason) || null;
765
+ const metadata = options.metadata && typeof options.metadata === 'object'
766
+ ? options.metadata
767
+ : {};
768
+ const issuedAt = normalizeIsoTimestamp(options.issued_at || options.issuedAt, this.now()) || this.now();
769
+ const ttlMinutes = normalizeInteger(options.ttl_minutes || options.ttlMinutes, 15);
770
+ const fallbackExpiresAt = new Date(
771
+ (Date.parse(issuedAt) || Date.now()) + (Math.max(ttlMinutes, 1) * 60 * 1000)
772
+ ).toISOString();
773
+ const expiresAt = normalizeIsoTimestamp(options.expires_at || options.expiresAt, fallbackExpiresAt) || fallbackExpiresAt;
774
+ const leaseId = normalizeString(options.lease_id || options.leaseId)
775
+ || `lease-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
776
+ const nowIso = this.now();
777
+
778
+ if (this._useMemoryBackend()) {
779
+ return this._issueAuthLeaseInMemory({
780
+ leaseId,
781
+ subject,
782
+ role,
783
+ scope,
784
+ reason,
785
+ metadata,
786
+ issuedAt,
787
+ expiresAt,
788
+ nowIso
789
+ });
790
+ }
791
+
792
+ if (!await this.ensureReady()) {
793
+ return null;
794
+ }
795
+
796
+ this._db
797
+ .prepare(`
798
+ INSERT OR REPLACE INTO auth_lease_registry(
799
+ lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
800
+ )
801
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
802
+ `)
803
+ .run(
804
+ leaseId,
805
+ subject,
806
+ role,
807
+ JSON.stringify(scope),
808
+ reason,
809
+ JSON.stringify(metadata),
810
+ issuedAt,
811
+ expiresAt,
812
+ nowIso,
813
+ nowIso
814
+ );
815
+
816
+ return this.getAuthLease(leaseId);
817
+ }
818
+
819
+ async getAuthLease(leaseId) {
820
+ const normalizedLeaseId = normalizeString(leaseId);
821
+ if (!normalizedLeaseId) {
822
+ return null;
823
+ }
824
+
825
+ if (this._useMemoryBackend()) {
826
+ const row = this._memory.auth_leases[normalizedLeaseId];
827
+ return row
828
+ ? {
829
+ ...row,
830
+ scope: normalizeStringArray(row.scope, ['project:*']),
831
+ metadata: { ...(row.metadata || {}) }
832
+ }
833
+ : null;
834
+ }
835
+
836
+ if (!await this.ensureReady()) {
837
+ return null;
838
+ }
839
+
840
+ const row = this._db
841
+ .prepare(`
842
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
843
+ FROM auth_lease_registry
844
+ WHERE lease_id = ?
845
+ `)
846
+ .get(normalizedLeaseId);
847
+ return this._mapAuthLeaseRow(row);
848
+ }
849
+
850
+ async listAuthLeases(options = {}) {
851
+ const activeOnly = options.activeOnly !== false;
852
+ const limit = normalizeInteger(options.limit, 20);
853
+ const nowIso = this.now();
854
+
855
+ if (this._useMemoryBackend()) {
856
+ let rows = Object.values(this._memory.auth_leases || {}).map((item) => ({
857
+ ...item,
858
+ scope: normalizeStringArray(item.scope, ['project:*']),
859
+ metadata: { ...(item.metadata || {}) }
860
+ }));
861
+ if (activeOnly) {
862
+ const nowTime = Date.parse(nowIso) || Date.now();
863
+ rows = rows.filter((item) => {
864
+ const revokedAt = normalizeString(item.revoked_at);
865
+ if (revokedAt) {
866
+ return false;
867
+ }
868
+ const expiresAt = Date.parse(item.expires_at || '') || 0;
869
+ return expiresAt > nowTime;
870
+ });
871
+ }
872
+ rows.sort((left, right) => (Date.parse(right.created_at || '') || 0) - (Date.parse(left.created_at || '') || 0));
873
+ return limit > 0 ? rows.slice(0, limit) : rows;
874
+ }
875
+
876
+ if (!await this.ensureReady()) {
877
+ return null;
878
+ }
879
+
880
+ const query = activeOnly
881
+ ? `
882
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
883
+ FROM auth_lease_registry
884
+ WHERE revoked_at IS NULL AND expires_at > ?
885
+ ORDER BY created_at DESC
886
+ LIMIT ?
887
+ `
888
+ : `
889
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
890
+ FROM auth_lease_registry
891
+ ORDER BY created_at DESC
892
+ LIMIT ?
893
+ `;
894
+
895
+ const statement = this._db.prepare(query);
896
+ const rows = activeOnly
897
+ ? statement.all(nowIso, limit)
898
+ : statement.all(limit);
899
+ return rows
900
+ .map((row) => this._mapAuthLeaseRow(row))
901
+ .filter(Boolean);
902
+ }
903
+
904
+ async revokeAuthLease(leaseId, options = {}) {
905
+ const normalizedLeaseId = normalizeString(leaseId);
906
+ if (!normalizedLeaseId) {
907
+ return null;
908
+ }
909
+ const revokedAt = normalizeIsoTimestamp(options.revoked_at || options.revokedAt, this.now()) || this.now();
910
+
911
+ if (this._useMemoryBackend()) {
912
+ const existing = this._memory.auth_leases[normalizedLeaseId];
913
+ if (!existing) {
914
+ return null;
915
+ }
916
+ existing.revoked_at = revokedAt;
917
+ existing.updated_at = revokedAt;
918
+ this._memory.auth_leases[normalizedLeaseId] = existing;
919
+ return {
920
+ ...existing,
921
+ scope: normalizeStringArray(existing.scope, ['project:*']),
922
+ metadata: { ...(existing.metadata || {}) }
923
+ };
924
+ }
925
+
926
+ if (!await this.ensureReady()) {
927
+ return null;
928
+ }
929
+
930
+ this._db
931
+ .prepare('UPDATE auth_lease_registry SET revoked_at = ?, updated_at = ? WHERE lease_id = ?')
932
+ .run(revokedAt, revokedAt, normalizedLeaseId);
933
+ return this.getAuthLease(normalizedLeaseId);
934
+ }
935
+
936
+ async appendAuthEvent(event = {}) {
937
+ const eventType = normalizeString(event.event_type || event.eventType);
938
+ if (!eventType) {
939
+ return false;
940
+ }
941
+ const eventId = normalizeString(event.event_id || event.eventId)
942
+ || `auth-evt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
943
+ const timestamp = normalizeIsoTimestamp(event.event_timestamp || event.timestamp, this.now()) || this.now();
944
+ const normalizedEvent = {
945
+ event_id: eventId,
946
+ event_timestamp: timestamp,
947
+ event_type: eventType,
948
+ action: normalizeString(event.action) || null,
949
+ actor: normalizeString(event.actor) || null,
950
+ lease_id: normalizeString(event.lease_id || event.leaseId) || null,
951
+ result: normalizeString(event.result) || null,
952
+ target: normalizeString(event.target) || null,
953
+ detail: event.detail && typeof event.detail === 'object'
954
+ ? event.detail
955
+ : {}
956
+ };
957
+
958
+ if (this._useMemoryBackend()) {
959
+ const existingIndex = this._memory.auth_events
960
+ .findIndex((item) => normalizeString(item.event_id) === eventId);
961
+ const row = {
962
+ ...normalizedEvent,
963
+ created_at: this.now()
964
+ };
965
+ if (existingIndex >= 0) {
966
+ this._memory.auth_events[existingIndex] = row;
967
+ } else {
968
+ this._memory.auth_events.push(row);
969
+ }
970
+ this._memory.auth_events.sort((left, right) => {
971
+ const l = Date.parse(left.event_timestamp || '') || 0;
972
+ const r = Date.parse(right.event_timestamp || '') || 0;
973
+ return l - r;
974
+ });
975
+ return true;
976
+ }
977
+
978
+ if (!await this.ensureReady()) {
979
+ return false;
980
+ }
981
+
982
+ this._db
983
+ .prepare(`
984
+ INSERT OR REPLACE INTO auth_event_stream(
985
+ event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
986
+ )
987
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
988
+ `)
989
+ .run(
990
+ normalizedEvent.event_id,
991
+ normalizedEvent.event_timestamp,
992
+ normalizedEvent.event_type,
993
+ normalizedEvent.action,
994
+ normalizedEvent.actor,
995
+ normalizedEvent.lease_id,
996
+ normalizedEvent.result,
997
+ normalizedEvent.target,
998
+ JSON.stringify(normalizedEvent.detail || {}),
999
+ this.now()
1000
+ );
1001
+ return true;
1002
+ }
1003
+
1004
+ async listAuthEvents(options = {}) {
1005
+ const limit = normalizeInteger(options.limit, 50);
1006
+
1007
+ if (this._useMemoryBackend()) {
1008
+ const rows = [...this._memory.auth_events]
1009
+ .sort((left, right) => (Date.parse(right.event_timestamp || '') || 0) - (Date.parse(left.event_timestamp || '') || 0))
1010
+ .map((row) => ({
1011
+ ...row,
1012
+ detail: row.detail && typeof row.detail === 'object' ? row.detail : {}
1013
+ }));
1014
+ return limit > 0 ? rows.slice(0, limit) : rows;
1015
+ }
1016
+
1017
+ if (!await this.ensureReady()) {
1018
+ return null;
1019
+ }
1020
+
1021
+ const query = limit > 0
1022
+ ? `
1023
+ SELECT event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
1024
+ FROM auth_event_stream
1025
+ ORDER BY event_timestamp DESC
1026
+ LIMIT ?
1027
+ `
1028
+ : `
1029
+ SELECT event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
1030
+ FROM auth_event_stream
1031
+ ORDER BY event_timestamp DESC
1032
+ `;
1033
+
1034
+ const statement = this._db.prepare(query);
1035
+ const rows = limit > 0 ? statement.all(limit) : statement.all();
1036
+ return rows
1037
+ .map((row) => this._mapAuthEventRow(row))
1038
+ .filter(Boolean);
1039
+ }
1040
+
1041
+ async upsertTimelineSnapshotIndex(records = [], options = {}) {
1042
+ const source = normalizeString(options.source) || 'file.timeline.index';
1043
+ const nowIso = this.now();
1044
+ const normalizedRecords = Array.isArray(records)
1045
+ ? records.map((item) => ({
1046
+ snapshot_id: normalizeString(item && item.snapshot_id),
1047
+ created_at: normalizeIsoTimestamp(item && item.created_at, nowIso) || nowIso,
1048
+ trigger: normalizeString(item && item.trigger) || null,
1049
+ event: normalizeString(item && item.event) || null,
1050
+ summary: normalizeString(item && item.summary) || null,
1051
+ scene_id: normalizeString(item && item.scene_id) || null,
1052
+ session_id: normalizeString(item && item.session_id) || null,
1053
+ command: normalizeString(item && item.command) || null,
1054
+ file_count: normalizeNonNegativeInteger(item && item.file_count, 0),
1055
+ total_bytes: normalizeNonNegativeInteger(item && item.total_bytes, 0),
1056
+ snapshot_path: normalizeString(item && (item.snapshot_path || item.path)) || null,
1057
+ git: item && item.git && typeof item.git === 'object' ? item.git : {},
1058
+ source,
1059
+ updated_at: nowIso
1060
+ }))
1061
+ .filter((item) => item.snapshot_id)
1062
+ : [];
1063
+
1064
+ if (this._useMemoryBackend()) {
1065
+ for (const item of normalizedRecords) {
1066
+ this._memory.timeline_snapshots[item.snapshot_id] = { ...item };
1067
+ }
1068
+ return {
1069
+ success: true,
1070
+ written: normalizedRecords.length,
1071
+ total: Object.keys(this._memory.timeline_snapshots || {}).length
1072
+ };
1073
+ }
1074
+
1075
+ if (!await this.ensureReady()) {
1076
+ return null;
1077
+ }
1078
+
1079
+ const statement = this._db.prepare(`
1080
+ INSERT OR REPLACE INTO timeline_snapshot_registry(
1081
+ snapshot_id, created_at, trigger, event, summary, scene_id, session_id, command,
1082
+ file_count, total_bytes, snapshot_path, git_json, source, updated_at
1083
+ )
1084
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1085
+ `);
1086
+
1087
+ this._withTransaction(() => {
1088
+ for (const item of normalizedRecords) {
1089
+ statement.run(
1090
+ item.snapshot_id,
1091
+ item.created_at,
1092
+ item.trigger,
1093
+ item.event,
1094
+ item.summary,
1095
+ item.scene_id,
1096
+ item.session_id,
1097
+ item.command,
1098
+ item.file_count,
1099
+ item.total_bytes,
1100
+ item.snapshot_path,
1101
+ JSON.stringify(item.git || {}),
1102
+ item.source,
1103
+ item.updated_at
1104
+ );
1105
+ }
1106
+ });
1107
+
1108
+ const totalRow = this._db
1109
+ .prepare('SELECT COUNT(*) AS total FROM timeline_snapshot_registry')
1110
+ .get();
1111
+
1112
+ return {
1113
+ success: true,
1114
+ written: normalizedRecords.length,
1115
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
1116
+ };
1117
+ }
1118
+
1119
+ async listTimelineSnapshotIndex(options = {}) {
1120
+ const limit = normalizeInteger(options.limit, 100);
1121
+ const triggerFilter = normalizeString(options.trigger);
1122
+ const snapshotIdFilter = normalizeString(options.snapshotId);
1123
+
1124
+ if (this._useMemoryBackend()) {
1125
+ let rows = Object.values(this._memory.timeline_snapshots || {}).map((item) => ({ ...item }));
1126
+ if (triggerFilter) {
1127
+ rows = rows.filter((item) => normalizeString(item.trigger) === triggerFilter);
1128
+ }
1129
+ if (snapshotIdFilter) {
1130
+ rows = rows.filter((item) => normalizeString(item.snapshot_id) === snapshotIdFilter);
1131
+ }
1132
+ rows.sort((left, right) => (Date.parse(right.created_at || '') || 0) - (Date.parse(left.created_at || '') || 0));
1133
+ if (limit > 0) {
1134
+ rows = rows.slice(0, limit);
1135
+ }
1136
+ return rows.map((row) => this._mapTimelineSnapshotRow({
1137
+ ...row,
1138
+ git_json: JSON.stringify(row.git || {})
1139
+ }));
1140
+ }
1141
+
1142
+ if (!await this.ensureReady()) {
1143
+ return null;
1144
+ }
1145
+
1146
+ let query = `
1147
+ SELECT snapshot_id, created_at, trigger, event, summary, scene_id, session_id, command,
1148
+ file_count, total_bytes, snapshot_path, git_json, source, updated_at
1149
+ FROM timeline_snapshot_registry
1150
+ `;
1151
+ const clauses = [];
1152
+ const params = [];
1153
+ if (triggerFilter) {
1154
+ clauses.push('trigger = ?');
1155
+ params.push(triggerFilter);
1156
+ }
1157
+ if (snapshotIdFilter) {
1158
+ clauses.push('snapshot_id = ?');
1159
+ params.push(snapshotIdFilter);
1160
+ }
1161
+ if (clauses.length > 0) {
1162
+ query += ` WHERE ${clauses.join(' AND ')}`;
1163
+ }
1164
+ query += ' ORDER BY created_at DESC';
1165
+ if (limit > 0) {
1166
+ query += ' LIMIT ?';
1167
+ params.push(limit);
1168
+ }
1169
+
1170
+ const rows = this._db.prepare(query).all(...params);
1171
+ return rows
1172
+ .map((row) => this._mapTimelineSnapshotRow(row))
1173
+ .filter(Boolean);
1174
+ }
1175
+
1176
+ async upsertSceneSessionCycles(records = [], options = {}) {
1177
+ const source = normalizeString(options.source) || 'file.session.scene-index';
1178
+ const nowIso = this.now();
1179
+ const normalizedRecords = Array.isArray(records)
1180
+ ? records.map((item) => ({
1181
+ scene_id: normalizeString(item && item.scene_id),
1182
+ cycle: normalizeNonNegativeInteger(item && item.cycle, 0),
1183
+ session_id: normalizeString(item && item.session_id),
1184
+ status: normalizeString(item && item.status) || null,
1185
+ started_at: normalizeIsoTimestamp(item && item.started_at, nowIso) || nowIso,
1186
+ completed_at: normalizeIsoTimestamp(item && item.completed_at, '') || null,
1187
+ source,
1188
+ updated_at: nowIso
1189
+ }))
1190
+ .filter((item) => item.scene_id && item.cycle > 0 && item.session_id)
1191
+ : [];
1192
+
1193
+ if (this._useMemoryBackend()) {
1194
+ for (const item of normalizedRecords) {
1195
+ const key = `${item.scene_id}::${item.cycle}`;
1196
+ this._memory.scene_session_cycles[key] = { ...item };
1197
+ }
1198
+ return {
1199
+ success: true,
1200
+ written: normalizedRecords.length,
1201
+ total: Object.keys(this._memory.scene_session_cycles || {}).length
1202
+ };
1203
+ }
1204
+
1205
+ if (!await this.ensureReady()) {
1206
+ return null;
1207
+ }
1208
+
1209
+ const statement = this._db.prepare(`
1210
+ INSERT OR REPLACE INTO scene_session_cycle_registry(
1211
+ scene_id, cycle, session_id, status, started_at, completed_at, source, updated_at
1212
+ )
1213
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?)
1214
+ `);
1215
+
1216
+ this._withTransaction(() => {
1217
+ for (const item of normalizedRecords) {
1218
+ statement.run(
1219
+ item.scene_id,
1220
+ item.cycle,
1221
+ item.session_id,
1222
+ item.status,
1223
+ item.started_at,
1224
+ item.completed_at,
1225
+ item.source,
1226
+ item.updated_at
1227
+ );
1228
+ }
1229
+ });
1230
+
1231
+ const totalRow = this._db
1232
+ .prepare('SELECT COUNT(*) AS total FROM scene_session_cycle_registry')
1233
+ .get();
1234
+
1235
+ return {
1236
+ success: true,
1237
+ written: normalizedRecords.length,
1238
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
1239
+ };
1240
+ }
1241
+
1242
+ async listSceneSessionCycles(options = {}) {
1243
+ const limit = normalizeInteger(options.limit, 100);
1244
+ const sceneId = normalizeString(options.sceneId);
1245
+
1246
+ if (this._useMemoryBackend()) {
1247
+ let rows = Object.values(this._memory.scene_session_cycles || {}).map((item) => ({ ...item }));
1248
+ if (sceneId) {
1249
+ rows = rows.filter((item) => normalizeString(item.scene_id) === sceneId);
1250
+ }
1251
+ rows.sort((left, right) => {
1252
+ const sceneCompare = `${left.scene_id}`.localeCompare(`${right.scene_id}`);
1253
+ if (sceneCompare !== 0) {
1254
+ return sceneCompare;
1255
+ }
1256
+ return right.cycle - left.cycle;
1257
+ });
1258
+ if (limit > 0) {
1259
+ rows = rows.slice(0, limit);
1260
+ }
1261
+ return rows.map((row) => this._mapSceneSessionCycleRow(row));
1262
+ }
1263
+
1264
+ if (!await this.ensureReady()) {
1265
+ return null;
1266
+ }
1267
+
1268
+ let query = `
1269
+ SELECT scene_id, cycle, session_id, status, started_at, completed_at, source, updated_at
1270
+ FROM scene_session_cycle_registry
1271
+ `;
1272
+ const params = [];
1273
+ if (sceneId) {
1274
+ query += ' WHERE scene_id = ?';
1275
+ params.push(sceneId);
1276
+ }
1277
+ query += ' ORDER BY scene_id ASC, cycle DESC';
1278
+ if (limit > 0) {
1279
+ query += ' LIMIT ?';
1280
+ params.push(limit);
1281
+ }
1282
+
1283
+ const rows = this._db.prepare(query).all(...params);
1284
+ return rows
1285
+ .map((row) => this._mapSceneSessionCycleRow(row))
1286
+ .filter(Boolean);
1287
+ }
1288
+
1289
+ async upsertAgentRuntimeRecords(records = [], options = {}) {
1290
+ const source = normalizeString(options.source) || 'file.agent-registry';
1291
+ const nowIso = this.now();
1292
+ const normalizedRecords = Array.isArray(records)
1293
+ ? records.map((item) => ({
1294
+ agent_id: normalizeString(item && item.agent_id),
1295
+ machine_id: normalizeString(item && item.machine_id) || null,
1296
+ instance_index: normalizeNonNegativeInteger(item && item.instance_index, 0),
1297
+ hostname: normalizeString(item && item.hostname) || null,
1298
+ registered_at: normalizeIsoTimestamp(item && item.registered_at, nowIso) || nowIso,
1299
+ last_heartbeat: normalizeIsoTimestamp(item && item.last_heartbeat, nowIso) || nowIso,
1300
+ status: normalizeString(item && item.status) || null,
1301
+ current_task: item && item.current_task && typeof item.current_task === 'object'
1302
+ ? item.current_task
1303
+ : null,
1304
+ source,
1305
+ updated_at: nowIso
1306
+ }))
1307
+ .filter((item) => item.agent_id)
1308
+ : [];
1309
+
1310
+ if (this._useMemoryBackend()) {
1311
+ for (const item of normalizedRecords) {
1312
+ this._memory.agent_runtime[item.agent_id] = { ...item };
1313
+ }
1314
+ return {
1315
+ success: true,
1316
+ written: normalizedRecords.length,
1317
+ total: Object.keys(this._memory.agent_runtime || {}).length
1318
+ };
1319
+ }
1320
+
1321
+ if (!await this.ensureReady()) {
1322
+ return null;
1323
+ }
1324
+
1325
+ const statement = this._db.prepare(`
1326
+ INSERT OR REPLACE INTO agent_runtime_registry(
1327
+ agent_id, machine_id, instance_index, hostname, registered_at, last_heartbeat, status, current_task_json, source, updated_at
1328
+ )
1329
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1330
+ `);
1331
+
1332
+ this._withTransaction(() => {
1333
+ for (const item of normalizedRecords) {
1334
+ statement.run(
1335
+ item.agent_id,
1336
+ item.machine_id,
1337
+ item.instance_index,
1338
+ item.hostname,
1339
+ item.registered_at,
1340
+ item.last_heartbeat,
1341
+ item.status,
1342
+ JSON.stringify(item.current_task),
1343
+ item.source,
1344
+ item.updated_at
1345
+ );
1346
+ }
1347
+ });
1348
+
1349
+ const totalRow = this._db
1350
+ .prepare('SELECT COUNT(*) AS total FROM agent_runtime_registry')
1351
+ .get();
1352
+
1353
+ return {
1354
+ success: true,
1355
+ written: normalizedRecords.length,
1356
+ total: normalizeNonNegativeInteger(totalRow && totalRow.total, 0)
1357
+ };
1358
+ }
1359
+
1360
+ async listAgentRuntimeRecords(options = {}) {
1361
+ const limit = normalizeInteger(options.limit, 100);
1362
+ const status = normalizeString(options.status);
1363
+
1364
+ if (this._useMemoryBackend()) {
1365
+ let rows = Object.values(this._memory.agent_runtime || {}).map((item) => ({ ...item }));
1366
+ if (status) {
1367
+ rows = rows.filter((item) => normalizeString(item.status) === status);
1368
+ }
1369
+ rows.sort((left, right) => (Date.parse(right.last_heartbeat || '') || 0) - (Date.parse(left.last_heartbeat || '') || 0));
1370
+ if (limit > 0) {
1371
+ rows = rows.slice(0, limit);
1372
+ }
1373
+ return rows.map((row) => this._mapAgentRuntimeRow({
1374
+ ...row,
1375
+ current_task_json: JSON.stringify(row.current_task || null)
1376
+ }));
1377
+ }
1378
+
1379
+ if (!await this.ensureReady()) {
1380
+ return null;
1381
+ }
1382
+
1383
+ let query = `
1384
+ SELECT agent_id, machine_id, instance_index, hostname, registered_at, last_heartbeat, status, current_task_json, source, updated_at
1385
+ FROM agent_runtime_registry
1386
+ `;
1387
+ const params = [];
1388
+ if (status) {
1389
+ query += ' WHERE status = ?';
1390
+ params.push(status);
1391
+ }
1392
+ query += ' ORDER BY last_heartbeat DESC';
1393
+ if (limit > 0) {
1394
+ query += ' LIMIT ?';
1395
+ params.push(limit);
1396
+ }
1397
+
1398
+ const rows = this._db.prepare(query).all(...params);
1399
+ return rows
1400
+ .map((row) => this._mapAgentRuntimeRow(row))
1401
+ .filter(Boolean);
1402
+ }
1403
+
1404
+ async appendStateMigrationRecord(record = {}) {
1405
+ const componentId = normalizeString(record.component_id || record.componentId);
1406
+ if (!componentId) {
1407
+ return null;
1408
+ }
1409
+ const migrationId = normalizeString(record.migration_id || record.migrationId)
1410
+ || `migration-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
1411
+ const startedAt = normalizeIsoTimestamp(record.started_at || record.startedAt, this.now()) || this.now();
1412
+ const completedAt = normalizeIsoTimestamp(record.completed_at || record.completedAt, '') || null;
1413
+ const nowIso = this.now();
1414
+ const normalized = {
1415
+ migration_id: migrationId,
1416
+ component_id: componentId,
1417
+ source_path: normalizeString(record.source_path || record.sourcePath) || null,
1418
+ mode: normalizeString(record.mode) || 'unknown',
1419
+ status: normalizeString(record.status) || 'completed',
1420
+ metrics: record.metrics && typeof record.metrics === 'object' ? record.metrics : {},
1421
+ detail: record.detail && typeof record.detail === 'object' ? record.detail : {},
1422
+ started_at: startedAt,
1423
+ completed_at: completedAt,
1424
+ updated_at: nowIso
1425
+ };
1426
+
1427
+ if (this._useMemoryBackend()) {
1428
+ this._memory.migration_records[normalized.migration_id] = { ...normalized };
1429
+ return { ...normalized };
1430
+ }
1431
+
1432
+ if (!await this.ensureReady()) {
1433
+ return null;
1434
+ }
1435
+
1436
+ this._db
1437
+ .prepare(`
1438
+ INSERT OR REPLACE INTO state_migration_registry(
1439
+ migration_id, component_id, source_path, mode, status, metrics_json, detail_json, started_at, completed_at, updated_at
1440
+ )
1441
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1442
+ `)
1443
+ .run(
1444
+ normalized.migration_id,
1445
+ normalized.component_id,
1446
+ normalized.source_path,
1447
+ normalized.mode,
1448
+ normalized.status,
1449
+ JSON.stringify(normalized.metrics || {}),
1450
+ JSON.stringify(normalized.detail || {}),
1451
+ normalized.started_at,
1452
+ normalized.completed_at,
1453
+ normalized.updated_at
1454
+ );
1455
+
1456
+ return this.listStateMigrations({ migrationId: normalized.migration_id, limit: 1 })
1457
+ .then((rows) => (Array.isArray(rows) && rows.length > 0 ? rows[0] : null));
1458
+ }
1459
+
1460
+ async listStateMigrations(options = {}) {
1461
+ const limit = normalizeInteger(options.limit, 50);
1462
+ const componentId = normalizeString(options.componentId);
1463
+ const migrationId = normalizeString(options.migrationId);
1464
+
1465
+ if (this._useMemoryBackend()) {
1466
+ let rows = Object.values(this._memory.migration_records || {}).map((item) => ({ ...item }));
1467
+ if (componentId) {
1468
+ rows = rows.filter((item) => normalizeString(item.component_id) === componentId);
1469
+ }
1470
+ if (migrationId) {
1471
+ rows = rows.filter((item) => normalizeString(item.migration_id) === migrationId);
1472
+ }
1473
+ rows.sort((left, right) => (Date.parse(right.started_at || '') || 0) - (Date.parse(left.started_at || '') || 0));
1474
+ if (limit > 0) {
1475
+ rows = rows.slice(0, limit);
1476
+ }
1477
+ return rows.map((row) => this._mapStateMigrationRow({
1478
+ ...row,
1479
+ metrics_json: JSON.stringify(row.metrics || {}),
1480
+ detail_json: JSON.stringify(row.detail || {})
1481
+ }));
1482
+ }
1483
+
1484
+ if (!await this.ensureReady()) {
1485
+ return null;
1486
+ }
1487
+
1488
+ let query = `
1489
+ SELECT migration_id, component_id, source_path, mode, status, metrics_json, detail_json, started_at, completed_at, updated_at
1490
+ FROM state_migration_registry
1491
+ `;
1492
+ const clauses = [];
1493
+ const params = [];
1494
+ if (componentId) {
1495
+ clauses.push('component_id = ?');
1496
+ params.push(componentId);
1497
+ }
1498
+ if (migrationId) {
1499
+ clauses.push('migration_id = ?');
1500
+ params.push(migrationId);
1501
+ }
1502
+ if (clauses.length > 0) {
1503
+ query += ` WHERE ${clauses.join(' AND ')}`;
1504
+ }
1505
+ query += ' ORDER BY started_at DESC';
1506
+ if (limit > 0) {
1507
+ query += ' LIMIT ?';
1508
+ params.push(limit);
1509
+ }
1510
+
1511
+ const rows = this._db.prepare(query).all(...params);
1512
+ return rows
1513
+ .map((row) => this._mapStateMigrationRow(row))
1514
+ .filter(Boolean);
1515
+ }
1516
+
513
1517
  _resolveOrCreateTaskRefInMemory(options = {}) {
514
1518
  const sceneId = normalizeString(options.sceneId);
515
1519
  const specId = normalizeString(options.specId);
@@ -566,6 +1570,30 @@ class SceStateStore {
566
1570
  this._memory.refs[taskRef] = row;
567
1571
  return { ...row, metadata: { ...(row.metadata || {}) } };
568
1572
  }
1573
+
1574
+ _issueAuthLeaseInMemory(options = {}) {
1575
+ const leaseId = normalizeString(options.leaseId)
1576
+ || `lease-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1577
+ const row = {
1578
+ lease_id: leaseId,
1579
+ subject: normalizeString(options.subject) || 'unknown',
1580
+ role: normalizeString(options.role) || 'maintainer',
1581
+ scope: normalizeStringArray(options.scope, ['project:*']),
1582
+ reason: normalizeString(options.reason) || null,
1583
+ metadata: options.metadata && typeof options.metadata === 'object' ? { ...options.metadata } : {},
1584
+ issued_at: normalizeIsoTimestamp(options.issuedAt, this.now()) || this.now(),
1585
+ expires_at: normalizeIsoTimestamp(options.expiresAt, this.now()) || this.now(),
1586
+ revoked_at: null,
1587
+ created_at: normalizeIsoTimestamp(options.nowIso, this.now()) || this.now(),
1588
+ updated_at: normalizeIsoTimestamp(options.nowIso, this.now()) || this.now()
1589
+ };
1590
+ this._memory.auth_leases[leaseId] = row;
1591
+ return {
1592
+ ...row,
1593
+ scope: normalizeStringArray(row.scope, ['project:*']),
1594
+ metadata: { ...(row.metadata || {}) }
1595
+ };
1596
+ }
569
1597
  }
570
1598
 
571
1599
  const STORE_CACHE = new Map();