@myagentroam/node 0.9.120 → 0.9.122

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/dist/connector.js CHANGED
@@ -1778,14 +1778,14 @@ export class NodeConnector {
1778
1778
  this.hostedConversationRuntime = undefined;
1779
1779
  this.hostedConversationOperations = undefined;
1780
1780
  this.conversationHostingMaintenanceOperations = undefined;
1781
- void this.workspaceUploadService.shutdown();
1781
+ const uploadDrain = this.workspaceUploadService.shutdown();
1782
1782
  this.workspaceFileService.dispose();
1783
1783
  this.workspaceWatchService.clearAll();
1784
1784
  this.workspaceWorkbenchService.clear();
1785
1785
  this.workspaceSessionService.clear();
1786
1786
  this.workspaceChangeService.clear();
1787
1787
  const runnerDrain = this.runners.stop();
1788
- void this.goalMcpServer.close();
1788
+ const goalMcpDrain = this.goalMcpServer.close();
1789
1789
  // close() transitions metadata before awaiting process-tree termination,
1790
1790
  // so the lifecycle is persisted before this synchronous shutdown returns.
1791
1791
  const terminalDrain = this.terminalManager.shutdown();
@@ -1796,13 +1796,20 @@ export class NodeConnector {
1796
1796
  this.controlChannel.stop();
1797
1797
  const database = this.database;
1798
1798
  this.database = undefined;
1799
+ const drains = Promise.allSettled([
1800
+ marAgentDrain,
1801
+ runnerDrain,
1802
+ terminalDrain,
1803
+ uploadDrain,
1804
+ goalMcpDrain
1805
+ ]);
1799
1806
  this.stopping = (async () => {
1800
1807
  try {
1801
1808
  await conversationHostingMaintenanceDrain;
1802
1809
  }
1803
1810
  finally {
1804
1811
  await hostedConversationRuntime?.dispose().catch(() => undefined);
1805
- await Promise.allSettled([marAgentDrain, runnerDrain, terminalDrain]);
1812
+ await drains;
1806
1813
  await this.gitExecutionService?.close();
1807
1814
  database?.close();
1808
1815
  }
package/dist/database.js CHANGED
@@ -4,16 +4,10 @@ import { dirname } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
6
6
  import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
7
- import { assertNodeSchemaV4, NODE_SCHEMA_V4, NODE_SCHEMA_VERSION_V4 } from './migrations/v004.js';
8
- import { assertNodeSchemaV5, NODE_SCHEMA_MIGRATION_V5, NODE_SCHEMA_VERSION_V5 } from './migrations/v005.js';
9
- import { assertNodeSchemaV6, NODE_SCHEMA_MIGRATION_V6, NODE_SCHEMA_VERSION_V6 } from './migrations/v006.js';
10
- import { assertNodeSchemaV7, NODE_SCHEMA_MIGRATION_V7, NODE_SCHEMA_VERSION_V7 } from './migrations/v007.js';
11
- import { assertNodeSchemaV8, NODE_SCHEMA_MIGRATION_V8, NODE_SCHEMA_VERSION_V8 } from './migrations/v008.js';
12
- import { assertNodeSchemaV9, isDraftNodeSchemaV9, NODE_SCHEMA_MIGRATION_V9, NODE_SCHEMA_MIGRATION_V9_FINALIZE, NODE_SCHEMA_VERSION_V9 } from './migrations/v009.js';
13
- import { assertNodeSchemaV10, NODE_SCHEMA_MIGRATION_V10, NODE_SCHEMA_VERSION_V10 } from './migrations/v010.js';
14
- import { assertNodeSchemaV11, NODE_SCHEMA_MIGRATION_V11, NODE_SCHEMA_VERSION_V11 } from './migrations/v011.js';
15
- import { assertNodeSchemaV12, NODE_SCHEMA_MIGRATION_V12, NODE_SCHEMA_VERSION_V12 } from './migrations/v012.js';
16
- const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V12;
7
+ import { assertNodeSchemaV13, NODE_SCHEMA_V13, NODE_SCHEMA_VERSION_V13 } from './migrations/v013.js';
8
+ // Future released migrations must be contiguous, immutable and begin at v14; Demo is excluded.
9
+ export const NODE_SCHEMA_MIGRATIONS = [];
10
+ const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
17
11
  export class NodeDatabase {
18
12
  now;
19
13
  raw;
@@ -28,8 +22,15 @@ export class NodeDatabase {
28
22
  timeout: 5_000,
29
23
  defensive: true
30
24
  });
31
- this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
32
- migrate(this.raw);
25
+ try {
26
+ // Refuse unsupported databases before changing the on-disk journal mode.
27
+ migrate(this.raw);
28
+ this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
29
+ }
30
+ catch (error) {
31
+ this.raw.close();
32
+ throw error;
33
+ }
33
34
  }
34
35
  finally {
35
36
  releaseMigrationLock();
@@ -335,7 +336,10 @@ export class NodeDatabase {
335
336
  return (Number(this.raw.prepare('DELETE FROM agent_sessions WHERE id = ?').run(sessionId).changes) > 0);
336
337
  }
337
338
  }
338
- function migrate(database) {
339
+ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
340
+ if (catalog.some((entry, index) => entry.version !== NODE_SCHEMA_VERSION_V13 + index + 1))
341
+ throw new Error('NODE_DATABASE_MIGRATION_CATALOG_INVALID');
342
+ const latest = NODE_SCHEMA_VERSION_V13 + catalog.length;
339
343
  const existingMigrationTable = database
340
344
  .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'node_schema_migrations'")
341
345
  .get();
@@ -347,10 +351,11 @@ function migrate(database) {
347
351
  throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
348
352
  database.exec('BEGIN IMMEDIATE');
349
353
  try {
350
- database.exec(NODE_SCHEMA_V4);
354
+ database.exec(NODE_SCHEMA_V13);
351
355
  database
352
356
  .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
353
- .run(NODE_SCHEMA_VERSION_V4, Date.now());
357
+ .run(NODE_SCHEMA_VERSION_V13, Date.now());
358
+ assertNodeSchemaV13(database);
354
359
  database.exec('COMMIT');
355
360
  }
356
361
  catch (error) {
@@ -358,80 +363,24 @@ function migrate(database) {
358
363
  throw error;
359
364
  }
360
365
  }
361
- const migrations = database
366
+ const versions = database
362
367
  .prepare('SELECT version FROM node_schema_migrations ORDER BY version')
363
- .all();
364
- const versions = migrations.map((row) => numberValue(row, 'version'));
365
- const current = versions.at(-1);
366
- if (current === undefined ||
367
- current < NODE_SCHEMA_VERSION_V4 ||
368
- current > NODE_SCHEMA_VERSION ||
369
- !versions.includes(NODE_SCHEMA_VERSION_V4))
370
- throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
371
- if (versions.some((version, index) => index > 0 && version !== (versions[index - 1] ?? 0) + 1))
372
- throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
373
- assertNodeSchemaV4(database);
374
- if (current === NODE_SCHEMA_VERSION_V4) {
375
- database.exec('BEGIN IMMEDIATE');
376
- try {
377
- database.exec(NODE_SCHEMA_MIGRATION_V5);
378
- database
379
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
380
- .run(NODE_SCHEMA_VERSION_V5, Date.now());
381
- database.exec('COMMIT');
382
- }
383
- catch (error) {
384
- database.exec('ROLLBACK');
385
- throw error;
386
- }
387
- }
388
- assertNodeSchemaV5(database);
389
- const migrated = database
390
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
391
- .get();
392
- if (numberValue(migrated, 'version') === NODE_SCHEMA_VERSION_V5) {
393
- database.exec('BEGIN IMMEDIATE');
394
- try {
395
- database.exec(NODE_SCHEMA_MIGRATION_V6);
396
- database
397
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
398
- .run(NODE_SCHEMA_VERSION_V6, Date.now());
399
- database.exec('COMMIT');
400
- }
401
- catch (error) {
402
- database.exec('ROLLBACK');
403
- throw error;
404
- }
405
- }
406
- const migratedV6 = database
407
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
408
- .get();
409
- if (numberValue(migratedV6, 'version') === NODE_SCHEMA_VERSION_V6) {
410
- assertNodeSchemaV6(database);
411
- database.exec('BEGIN IMMEDIATE');
412
- try {
413
- database.exec(NODE_SCHEMA_MIGRATION_V7);
414
- database
415
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
416
- .run(NODE_SCHEMA_VERSION_V7, Date.now());
417
- database.exec('COMMIT');
418
- }
419
- catch (error) {
420
- database.exec('ROLLBACK');
421
- throw error;
422
- }
423
- }
424
- assertNodeSchemaV7(database);
425
- const migratedV7 = database
426
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
427
- .get();
428
- if (numberValue(migratedV7, 'version') === NODE_SCHEMA_VERSION_V7) {
368
+ .all().map((row) => numberValue(row, 'version'));
369
+ const firstLegacyVersion = versions[0];
370
+ if (firstLegacyVersion !== undefined &&
371
+ firstLegacyVersion >= 1 &&
372
+ firstLegacyVersion <= 4 &&
373
+ versions.length === LEGACY_NODE_SCHEMA_V12 - firstLegacyVersion + 1 &&
374
+ versions.every((version, index) => version === firstLegacyVersion + index)) {
375
+ // v13 is a history-compaction baseline. The final v12 schema is still the v13 schema;
376
+ // validate it before atomically replacing only the migration ledger.
377
+ assertNodeSchemaV13(database);
429
378
  database.exec('BEGIN IMMEDIATE');
430
379
  try {
431
- database.exec(NODE_SCHEMA_MIGRATION_V8);
380
+ database.prepare('DELETE FROM node_schema_migrations').run();
432
381
  database
433
382
  .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
434
- .run(NODE_SCHEMA_VERSION_V8, Date.now());
383
+ .run(NODE_SCHEMA_VERSION_V13, Date.now());
435
384
  database.exec('COMMIT');
436
385
  }
437
386
  catch (error) {
@@ -439,37 +388,28 @@ function migrate(database) {
439
388
  throw error;
440
389
  }
441
390
  }
442
- assertNodeSchemaV8(database);
443
- const migratedV8 = database
444
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
445
- .get();
446
- const migratedV8Version = numberValue(migratedV8, 'version');
447
- if (migratedV8Version <= NODE_SCHEMA_VERSION_V9) {
448
- if (migratedV8Version === NODE_SCHEMA_VERSION_V8)
449
- applyNodeSchemaV9Migration(database, NODE_SCHEMA_MIGRATION_V9, true);
450
- else if (isDraftNodeSchemaV9(database))
451
- applyNodeSchemaV9Migration(database, NODE_SCHEMA_MIGRATION_V9_FINALIZE, false);
452
- assertNodeSchemaV9(database);
453
- applyNodeSchemaV10Migration(database);
454
- }
455
- assertNodeSchemaV10(database);
456
- const migratedV10 = database
457
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
458
- .get();
459
- if (numberValue(migratedV10, 'version') === NODE_SCHEMA_VERSION_V10)
460
- applyNodeSchemaV11Migration(database);
461
- assertNodeSchemaV11(database);
462
- const migratedV11 = database
463
- .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
464
- .get();
465
- if (numberValue(migratedV11, 'version') === NODE_SCHEMA_VERSION_V11) {
391
+ const migratedVersions = database
392
+ .prepare('SELECT version FROM node_schema_migrations ORDER BY version')
393
+ .all().map((row) => numberValue(row, 'version'));
394
+ const current = migratedVersions.at(-1);
395
+ if (current === undefined ||
396
+ current < NODE_SCHEMA_VERSION_V13 ||
397
+ current > latest ||
398
+ migratedVersions[0] !== NODE_SCHEMA_VERSION_V13)
399
+ throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
400
+ if (migratedVersions.some((version, index) => version !== NODE_SCHEMA_VERSION_V13 + index))
401
+ throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
402
+ assertNodeSchemaV13(database);
403
+ for (const entry of catalog) {
404
+ if (entry.version <= current)
405
+ continue;
466
406
  database.exec('BEGIN IMMEDIATE');
467
407
  try {
468
- database.exec(NODE_SCHEMA_MIGRATION_V12);
408
+ database.exec(entry.sql);
409
+ entry.assert(database);
469
410
  database
470
411
  .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
471
- .run(NODE_SCHEMA_VERSION_V12, Date.now());
472
- assertNodeSchemaV12(database);
412
+ .run(entry.version, Date.now());
473
413
  database.exec('COMMIT');
474
414
  }
475
415
  catch (error) {
@@ -477,62 +417,8 @@ function migrate(database) {
477
417
  throw error;
478
418
  }
479
419
  }
480
- assertNodeSchemaV12(database);
481
- }
482
- function applyNodeSchemaV9Migration(database, migration, recordVersion) {
483
- database.exec('PRAGMA foreign_keys = OFF');
484
- try {
485
- database.exec('BEGIN IMMEDIATE');
486
- database.exec(migration);
487
- if (recordVersion)
488
- database
489
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
490
- .run(NODE_SCHEMA_VERSION_V9, Date.now());
491
- database.exec('COMMIT');
492
- }
493
- catch (error) {
494
- database.exec('ROLLBACK');
495
- throw error;
496
- }
497
- finally {
498
- database.exec('PRAGMA foreign_keys = ON');
499
- }
500
- }
501
- function applyNodeSchemaV10Migration(database) {
502
- database.exec('PRAGMA foreign_keys = OFF');
503
- try {
504
- database.exec('BEGIN IMMEDIATE');
505
- database.exec(NODE_SCHEMA_MIGRATION_V10);
506
- database
507
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
508
- .run(NODE_SCHEMA_VERSION_V10, Date.now());
509
- database.exec('COMMIT');
510
- }
511
- catch (error) {
512
- database.exec('ROLLBACK');
513
- throw error;
514
- }
515
- finally {
516
- database.exec('PRAGMA foreign_keys = ON');
517
- }
518
- }
519
- function applyNodeSchemaV11Migration(database) {
520
- database.exec('PRAGMA foreign_keys = OFF');
521
- try {
522
- database.exec('BEGIN IMMEDIATE');
523
- database.exec(NODE_SCHEMA_MIGRATION_V11);
524
- database
525
- .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
526
- .run(NODE_SCHEMA_VERSION_V11, Date.now());
527
- database.exec('COMMIT');
528
- }
529
- catch (error) {
530
- database.exec('ROLLBACK');
531
- throw error;
532
- }
533
- finally {
534
- database.exec('PRAGMA foreign_keys = ON');
535
- }
420
+ if (catalog.length > 0)
421
+ catalog[catalog.length - 1].assert(database);
536
422
  }
537
423
  function mcpInstallation(row) {
538
424
  const runtime = mcpRuntimeDescriptorSchema.parse(JSON.parse(stringValue(row, 'runtime')));
@@ -0,0 +1,9 @@
1
+ /** Example only: never import this migration into the production catalog. */
2
+ export const NODE_SCHEMA_MIGRATION_DEMO = {
3
+ version: 1,
4
+ sql: 'CREATE TABLE migration_demo_example (id INTEGER PRIMARY KEY) STRICT;',
5
+ assert: (database) => {
6
+ if (!database.prepare("SELECT 1 FROM sqlite_master WHERE name='migration_demo_example'").get())
7
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:migration_demo_example');
8
+ }
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,327 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export const NODE_SCHEMA_VERSION_V13 = 13;
3
+ /** Complete schema at the supported SQLite baseline; future changes use the migration catalog. */
4
+ export const NODE_SCHEMA_V13 = String.raw `CREATE TABLE "agent_sessions" (
5
+ id TEXT PRIMARY KEY,
6
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
7
+ runner TEXT NOT NULL CHECK (runner IN ('mar-agent', 'codex', 'claude-code', 'opencode')),
8
+ pin_order INTEGER CHECK (pin_order IS NULL OR pin_order >= 0),
9
+ metadata TEXT NOT NULL CHECK (json_valid(metadata))
10
+ ) STRICT;
11
+
12
+ CREATE TABLE "conversation_files" (
13
+ id TEXT PRIMARY KEY,
14
+ conversation_id TEXT NOT NULL REFERENCES hosted_conversations(id) ON DELETE CASCADE,
15
+ category TEXT NOT NULL CHECK (category IN ('UPLOAD','OUTPUT')),
16
+ relative_path TEXT NOT NULL,
17
+ display_name TEXT NOT NULL,
18
+ mime_type TEXT NOT NULL,
19
+ size INTEGER NOT NULL CHECK (size >= 0),
20
+ source TEXT NOT NULL CHECK (source IN ('USER','AGENT','SYSTEM')),
21
+ turn_id TEXT,
22
+ item_id TEXT,
23
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
24
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
25
+ UNIQUE (conversation_id, category, relative_path)
26
+ ) STRICT;
27
+
28
+ CREATE TABLE conversation_template_publish_jobs (
29
+ id TEXT PRIMARY KEY,
30
+ template_id TEXT NOT NULL,
31
+ draft_revision INTEGER NOT NULL CHECK (draft_revision > 0),
32
+ stage TEXT NOT NULL CHECK (
33
+ stage IN ('VALIDATING','ENUMERATING','COPYING','SCANNING','MANIFEST','SEALED','REGISTERED')
34
+ ),
35
+ status TEXT NOT NULL CHECK (status IN ('QUEUED','RUNNING','SUCCEEDED','FAILED')),
36
+ version_id TEXT REFERENCES conversation_template_versions(id) ON DELETE SET NULL,
37
+ error_code TEXT,
38
+ error_summary TEXT,
39
+ requested_by_user_id INTEGER NOT NULL CHECK (requested_by_user_id > 0),
40
+ draft TEXT NOT NULL CHECK (json_valid(draft)),
41
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
42
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
43
+ ) STRICT;
44
+
45
+ CREATE TABLE conversation_template_versions (
46
+ id TEXT PRIMARY KEY,
47
+ template_id TEXT NOT NULL,
48
+ version_number INTEGER NOT NULL CHECK (version_number > 0),
49
+ draft_revision INTEGER NOT NULL CHECK (draft_revision > 0),
50
+ status TEXT NOT NULL CHECK (status IN ('AVAILABLE','DISABLED','CORRUPTED','RETAINED')),
51
+ storage_key TEXT NOT NULL UNIQUE,
52
+ manifest TEXT NOT NULL CHECK (json_valid(manifest)),
53
+ content_hash TEXT NOT NULL CHECK (length(content_hash) = 64),
54
+ manifest_hash TEXT NOT NULL CHECK (length(manifest_hash) = 64),
55
+ file_count INTEGER NOT NULL CHECK (file_count >= 0),
56
+ total_bytes INTEGER NOT NULL CHECK (total_bytes >= 0),
57
+ host_policy_version INTEGER NOT NULL CHECK (host_policy_version > 0),
58
+ published_by_user_id INTEGER NOT NULL CHECK (published_by_user_id > 0),
59
+ published_at INTEGER NOT NULL CHECK (published_at >= 0),
60
+ reference_count INTEGER NOT NULL DEFAULT 0 CHECK (reference_count >= 0), mcp_config_hash TEXT NOT NULL DEFAULT '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945'
61
+ CHECK (length(mcp_config_hash) = 64),
62
+ UNIQUE (template_id, version_number)
63
+ ) STRICT;
64
+
65
+ CREATE TABLE hosted_conversation_messages (
66
+ conversation_id TEXT NOT NULL REFERENCES hosted_conversations(id) ON DELETE CASCADE,
67
+ client_message_id TEXT NOT NULL,
68
+ run_id TEXT NOT NULL,
69
+ status TEXT NOT NULL CHECK (status IN ('RUNNING','SUCCEEDED','FAILED','INTERRUPTED')),
70
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
71
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0), user_text_present INTEGER NOT NULL DEFAULT 1
72
+ CHECK (user_text_present IN (0,1)),
73
+ PRIMARY KEY (conversation_id, client_message_id)
74
+ ) STRICT;
75
+
76
+ CREATE TABLE "hosted_conversations" (
77
+ id TEXT PRIMARY KEY,
78
+ user_storage_key TEXT NOT NULL,
79
+ conversation_storage_key TEXT NOT NULL,
80
+ client_request_id TEXT,
81
+ node_id TEXT NOT NULL,
82
+ session_id TEXT NOT NULL,
83
+ template_id TEXT NOT NULL,
84
+ template_name TEXT NOT NULL,
85
+ active_version_id TEXT NOT NULL REFERENCES conversation_template_versions(id),
86
+ title TEXT NOT NULL,
87
+ status TEXT NOT NULL CHECK (status IN ('ACTIVE')),
88
+ model_id TEXT NOT NULL,
89
+ template_deleted INTEGER NOT NULL DEFAULT 0 CHECK (template_deleted IN (0,1)),
90
+ used_bytes INTEGER NOT NULL DEFAULT 0 CHECK (used_bytes >= 0),
91
+ max_bytes INTEGER NOT NULL CHECK (max_bytes > 0),
92
+ created_at INTEGER NOT NULL CHECK (created_at >= 0),
93
+ updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
94
+ last_activity_at INTEGER NOT NULL CHECK (last_activity_at >= 0),
95
+ pinned_at INTEGER CHECK (pinned_at IS NULL OR pinned_at >= 0),
96
+ title_source TEXT NOT NULL DEFAULT 'CUSTOM'
97
+ CHECK (title_source IN ('DEFAULT','AUTO','CUSTOM')),
98
+ UNIQUE (user_storage_key, conversation_storage_key),
99
+ UNIQUE (user_storage_key, client_request_id)
100
+ ) STRICT;
101
+
102
+ CREATE TABLE mcp_installations (
103
+ target_kind TEXT NOT NULL CHECK (target_kind IN ('NODE','WORKSPACE')),
104
+ target_id TEXT NOT NULL, catalog_entry_id TEXT NOT NULL, server_name TEXT NOT NULL,
105
+ display_name TEXT NOT NULL, version TEXT NOT NULL,
106
+ enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
107
+ runtime TEXT NOT NULL CHECK (json_valid(runtime)),
108
+ configuration TEXT NOT NULL CHECK (json_valid(configuration)), installed_at INTEGER NOT NULL,
109
+ PRIMARY KEY (target_kind,target_id,server_name),
110
+ CHECK ((target_kind='NODE' AND target_id='') OR (target_kind='WORKSPACE' AND target_id<>''))
111
+ ) STRICT;
112
+
113
+ CREATE TABLE node_schema_migrations (
114
+ version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL
115
+ ) STRICT;
116
+
117
+ CREATE TABLE "runner_defaults" (
118
+ runner TEXT PRIMARY KEY CHECK (runner IN ('mar-agent', 'codex', 'claude-code', 'opencode')),
119
+ model TEXT NOT NULL, effort TEXT NOT NULL, access TEXT NOT NULL
120
+ ) STRICT;
121
+
122
+ CREATE TABLE terminal_sessions (
123
+ id TEXT PRIMARY KEY, workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
124
+ profile_id TEXT NOT NULL,
125
+ state TEXT NOT NULL CHECK (state IN ('RUNNING', 'EXITED', 'CLOSED')),
126
+ exit_code INTEGER, rows INTEGER NOT NULL CHECK (rows > 0 AND rows <= 1000),
127
+ cols INTEGER NOT NULL CHECK (cols > 0 AND cols <= 1000),
128
+ latest_sequence INTEGER NOT NULL CHECK (latest_sequence >= 0),
129
+ created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
130
+ ) STRICT;
131
+
132
+ CREATE TABLE workspaces (
133
+ id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE,
134
+ kind TEXT NOT NULL CHECK (kind IN ('DIRECTORY', 'GIT_WORKSPACE')),
135
+ git_summary TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
136
+ , preserve_git_config INTEGER NOT NULL DEFAULT 1 CHECK (preserve_git_config IN (0,1))) STRICT;
137
+
138
+ CREATE INDEX conversation_files_conversation_updated
139
+ ON conversation_files(conversation_id, updated_at DESC, id);
140
+
141
+ CREATE INDEX conversation_template_publish_jobs_status
142
+ ON conversation_template_publish_jobs(status, updated_at);
143
+
144
+ CREATE INDEX conversation_template_publish_jobs_template
145
+ ON conversation_template_publish_jobs(template_id, created_at, id);
146
+
147
+ CREATE INDEX conversation_template_versions_template_status
148
+ ON conversation_template_versions(template_id, status, published_at);
149
+
150
+ CREATE INDEX hosted_conversations_template
151
+ ON hosted_conversations(template_id, status, id);
152
+
153
+ CREATE INDEX hosted_conversations_user_pin_activity
154
+ ON hosted_conversations(
155
+ user_storage_key,
156
+ (pinned_at IS NOT NULL) DESC,
157
+ last_activity_at DESC,
158
+ id DESC
159
+ );`;
160
+ export function assertNodeSchemaV13(database, exactBaseline = false) {
161
+ const required = {
162
+ agent_sessions: ['id', 'workspace_id', 'runner', 'pin_order', 'metadata'],
163
+ conversation_files: [
164
+ 'id',
165
+ 'conversation_id',
166
+ 'category',
167
+ 'relative_path',
168
+ 'display_name',
169
+ 'mime_type',
170
+ 'size',
171
+ 'source',
172
+ 'turn_id',
173
+ 'item_id',
174
+ 'created_at',
175
+ 'updated_at'
176
+ ],
177
+ conversation_template_publish_jobs: [
178
+ 'id',
179
+ 'template_id',
180
+ 'draft_revision',
181
+ 'stage',
182
+ 'status',
183
+ 'version_id',
184
+ 'error_code',
185
+ 'error_summary',
186
+ 'requested_by_user_id',
187
+ 'draft',
188
+ 'created_at',
189
+ 'updated_at'
190
+ ],
191
+ conversation_template_versions: [
192
+ 'id',
193
+ 'template_id',
194
+ 'version_number',
195
+ 'draft_revision',
196
+ 'status',
197
+ 'storage_key',
198
+ 'manifest',
199
+ 'content_hash',
200
+ 'manifest_hash',
201
+ 'file_count',
202
+ 'total_bytes',
203
+ 'host_policy_version',
204
+ 'published_by_user_id',
205
+ 'published_at',
206
+ 'reference_count',
207
+ 'mcp_config_hash'
208
+ ],
209
+ hosted_conversation_messages: [
210
+ 'conversation_id',
211
+ 'client_message_id',
212
+ 'run_id',
213
+ 'status',
214
+ 'created_at',
215
+ 'updated_at',
216
+ 'user_text_present'
217
+ ],
218
+ hosted_conversations: [
219
+ 'id',
220
+ 'user_storage_key',
221
+ 'conversation_storage_key',
222
+ 'client_request_id',
223
+ 'node_id',
224
+ 'session_id',
225
+ 'template_id',
226
+ 'template_name',
227
+ 'active_version_id',
228
+ 'title',
229
+ 'status',
230
+ 'model_id',
231
+ 'template_deleted',
232
+ 'used_bytes',
233
+ 'max_bytes',
234
+ 'created_at',
235
+ 'updated_at',
236
+ 'last_activity_at',
237
+ 'pinned_at',
238
+ 'title_source'
239
+ ],
240
+ mcp_installations: [
241
+ 'target_kind',
242
+ 'target_id',
243
+ 'catalog_entry_id',
244
+ 'server_name',
245
+ 'display_name',
246
+ 'version',
247
+ 'enabled',
248
+ 'runtime',
249
+ 'configuration',
250
+ 'installed_at'
251
+ ],
252
+ node_schema_migrations: ['version', 'applied_at'],
253
+ runner_defaults: ['runner', 'model', 'effort', 'access'],
254
+ terminal_sessions: [
255
+ 'id',
256
+ 'workspace_id',
257
+ 'profile_id',
258
+ 'state',
259
+ 'exit_code',
260
+ 'rows',
261
+ 'cols',
262
+ 'latest_sequence',
263
+ 'created_at',
264
+ 'updated_at'
265
+ ],
266
+ workspaces: [
267
+ 'id',
268
+ 'path',
269
+ 'kind',
270
+ 'git_summary',
271
+ 'created_at',
272
+ 'updated_at',
273
+ 'preserve_git_config'
274
+ ]
275
+ };
276
+ for (const [table, expected] of Object.entries(required)) {
277
+ const columns = database.prepare(`PRAGMA table_info(${table})`).all();
278
+ if (expected.some((name) => !columns.some((column) => column.name === name)))
279
+ throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${table}`);
280
+ }
281
+ for (const name of [
282
+ 'conversation_files_conversation_updated',
283
+ 'conversation_template_publish_jobs_status',
284
+ 'conversation_template_publish_jobs_template',
285
+ 'conversation_template_versions_template_status',
286
+ 'hosted_conversations_template',
287
+ 'hosted_conversations_user_pin_activity'
288
+ ]) {
289
+ if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type='index' AND name=?").get(name))
290
+ throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${name}`);
291
+ }
292
+ const workspace = database
293
+ .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='workspaces'")
294
+ .get();
295
+ if (typeof workspace?.sql !== 'string' ||
296
+ !workspace.sql.includes('preserve_git_config INTEGER NOT NULL DEFAULT 1'))
297
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:workspaces');
298
+ for (const name of ['agent_sessions', 'runner_defaults']) {
299
+ const row = database
300
+ .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?")
301
+ .get(name);
302
+ const definition = row?.sql;
303
+ if (typeof definition !== 'string' ||
304
+ !["'mar-agent'", "'codex'", "'claude-code'", "'opencode'"].every((runner) => definition.includes(runner)))
305
+ throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${name}:runner`);
306
+ }
307
+ if (database.prepare('PRAGMA foreign_key_check').all().length !== 0)
308
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:foreign_keys');
309
+ if (exactBaseline) {
310
+ // Only compare whole DDL at v13; later additive migrations may extend these tables.
311
+ const reference = new DatabaseSync(':memory:');
312
+ try {
313
+ reference.exec(NODE_SCHEMA_V13);
314
+ const query = "SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name";
315
+ const expected = reference.prepare(query).all();
316
+ const actual = database.prepare(query).all();
317
+ if (actual.length !== expected.length ||
318
+ expected.some((entry, index) => actual[index]?.type !== entry.type ||
319
+ actual[index]?.name !== entry.name ||
320
+ actual[index]?.sql !== entry.sql))
321
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:baseline');
322
+ }
323
+ finally {
324
+ reference.close();
325
+ }
326
+ }
327
+ }
@@ -244,6 +244,7 @@ export class MarAgentManagedRunController {
244
244
  let executionModels = [];
245
245
  try {
246
246
  const sessionKey = input.externalSessionId ?? input.sessionId;
247
+ // Reserve both identities while opening so a concurrent release can await the same runtime.
247
248
  const acquisitionKeys = [...new Set([sessionKey, input.sessionId])];
248
249
  if (acquisitionKeys.some((key) => this.#runtimeAcquisitions.get(key) !== undefined))
249
250
  throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');