@myagentroam/node 0.9.126 → 0.9.128

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
@@ -66,6 +66,8 @@ import { ScheduledSessionCoordinator } from './service/scheduled-session-coordin
66
66
  import { ScheduledSessionStore } from './service/scheduled-session-store.js';
67
67
  import { ScheduledRunAuthorizationControl } from './service/scheduled-run-authorization-control.js';
68
68
  import { ScheduledTaskMcpServer } from './service/scheduled-task-mcp-server.js';
69
+ import { MailNotificationControl } from './service/mail-notification-control.js';
70
+ import { MailNotificationMcpServer, resolveMailNotificationCapability } from './service/mail-notification-mcp-server.js';
69
71
  import { SessionCatalogService } from './service/session-catalog-service.js';
70
72
  import { SessionCommandService } from './service/session-command-service.js';
71
73
  import { SessionPresentationService } from './service/session-presentation-service.js';
@@ -157,6 +159,7 @@ export class NodeConnector {
157
159
  controlChannel;
158
160
  controlMessages;
159
161
  scheduledRunAuthorization;
162
+ mailNotificationControl = new MailNotificationControl((envelope) => this.sendEnvelope(envelope));
160
163
  scheduledSessions;
161
164
  nodeRequests;
162
165
  connectionLifecycle;
@@ -288,6 +291,7 @@ export class NodeConnector {
288
291
  throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
289
292
  return this.sessionScheduleService.createFromMcp(input);
290
293
  }, (sessionId) => this.runtime.getAgentSession(sessionId)?.externalSessionId ?? sessionId);
294
+ mailNotificationMcpServer = new MailNotificationMcpServer((capability) => resolveMailNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request));
291
295
  runnerUsageReader;
292
296
  terminalManager;
293
297
  upgradeCoordinator;
@@ -343,9 +347,15 @@ export class NodeConnector {
343
347
  return;
344
348
  const store = new ScheduledSessionStore(this.database.raw);
345
349
  store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
346
- if (status !== 'SUCCEEDED') {
347
- const task = store.get(run.sessionId);
348
- if (task !== undefined)
350
+ const task = store.getById(run.clientMessageId.split(':', 1)[0]);
351
+ if (task !== undefined) {
352
+ if (task.notifyOnTerminal)
353
+ this.publishScheduledTaskNotification(task, run.clientMessageId, {
354
+ kind: 'SCHEDULE_TERMINAL',
355
+ outcome: status,
356
+ runId: run.id
357
+ });
358
+ else if (status !== 'SUCCEEDED')
349
359
  this.emitScheduledNotification(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
350
360
  }
351
361
  },
@@ -422,6 +432,7 @@ export class NodeConnector {
422
432
  this.goalExecutionProgress.delete(executionId);
423
433
  this.goalMcpServer.revoke(executionId);
424
434
  this.scheduledTaskMcpServer.revoke(executionId);
435
+ this.mailNotificationMcpServer.revoke(executionId);
425
436
  this.continuousRunCoordinator.remove(executionId);
426
437
  }
427
438
  });
@@ -831,6 +842,34 @@ export class NodeConnector {
831
842
  },
832
843
  secrets: resolved.secrets
833
844
  };
845
+ },
846
+ mailMcp: async (input) => {
847
+ const run = this.runtime.getRun(input.runId);
848
+ if (run?.initiatedByUserId === undefined)
849
+ return undefined;
850
+ const resolved = await this.mailNotificationMcpServer.installation({
851
+ executionId: input.runId,
852
+ sessionId: input.sessionId,
853
+ workspaceId: input.workspaceId,
854
+ userId: run.initiatedByUserId,
855
+ source: run.source === 'SCHEDULED' ? 'SCHEDULED' : 'MANUAL'
856
+ });
857
+ return {
858
+ installation: {
859
+ targetKind: 'WORKSPACE',
860
+ targetId: input.workspaceId,
861
+ catalogEntryId: 'mar-internal-mail',
862
+ serverName: 'mar_notification',
863
+ displayName: 'MAR email notification',
864
+ version: '1.0.0',
865
+ enabled: true,
866
+ runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
867
+ configuration: resolved.installation,
868
+ installedAt: Date.now(),
869
+ ...resolved.installation
870
+ },
871
+ secrets: resolved.secrets
872
+ };
834
873
  }
835
874
  });
836
875
  this.workspaceQueueWorkbenchService = new WorkspaceQueueWorkbenchService({
@@ -1191,6 +1230,8 @@ export class NodeConnector {
1191
1230
  this.directTransferService?.close();
1192
1231
  this.runtimeCredentials.disconnected();
1193
1232
  this.scheduledRunAuthorization.disconnected();
1233
+ this.mailNotificationControl.disconnected();
1234
+ this.mailNotificationMcpServer.revokeAll();
1194
1235
  this.terminalChannel.disconnect();
1195
1236
  if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
1196
1237
  nodeLog('node.connection.replaced', {
@@ -1209,6 +1250,7 @@ export class NodeConnector {
1209
1250
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
1210
1251
  handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
1211
1252
  handleScheduledRunAuthorizeResponse: (envelope) => this.scheduledRunAuthorization.handle(envelope),
1253
+ handleMailNotificationResponse: (envelope) => this.mailNotificationControl.handle(envelope),
1212
1254
  handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
1213
1255
  handleTerminalDirect: (envelope) => this.handleTerminalDirect(envelope),
1214
1256
  reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
@@ -1271,6 +1313,7 @@ export class NodeConnector {
1271
1313
  onConnected: () => {
1272
1314
  this.runtimeCredentials.connected();
1273
1315
  this.scheduledRunAuthorization.connected();
1316
+ this.mailNotificationControl.connected();
1274
1317
  this.recentRunProjectionService.publishSnapshot();
1275
1318
  }
1276
1319
  });
@@ -1923,6 +1966,8 @@ export class NodeConnector {
1923
1966
  this.runtimeCredentials.close();
1924
1967
  this.scheduledSessions.stop();
1925
1968
  this.scheduledRunAuthorization.close();
1969
+ this.mailNotificationControl.close();
1970
+ const mailMcpDrain = this.mailNotificationMcpServer.close();
1926
1971
  this.directTransferService?.dispose();
1927
1972
  this.directTransferService = undefined;
1928
1973
  this.controlChannel.stop();
@@ -1934,7 +1979,8 @@ export class NodeConnector {
1934
1979
  terminalDrain,
1935
1980
  uploadDrain,
1936
1981
  goalMcpDrain,
1937
- scheduledMcpDrain
1982
+ scheduledMcpDrain,
1983
+ mailMcpDrain
1938
1984
  ]);
1939
1985
  this.stopping = (async () => {
1940
1986
  try {
@@ -1977,24 +2023,32 @@ export class NodeConnector {
1977
2023
  }
1978
2024
  });
1979
2025
  };
1980
- emitScheduledNotification = (task, occurrenceId, code, status) => {
2026
+ emitScheduledNotification = (task, occurrenceId, code, status) => this.publishScheduledTaskNotification(task, occurrenceId, {
2027
+ kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
2028
+ code
2029
+ });
2030
+ publishScheduledTaskNotification = (task, occurrenceId, event) => {
1981
2031
  const occurredAt = Date.now();
2032
+ const recipient = (this.database === undefined
2033
+ ? undefined
2034
+ : new ScheduledSessionStore(this.database.raw).getById(task.id))?.ownerUserId ?? task.ownerUserId;
1982
2035
  const session = this.runtime.getAgentSession(task.sessionId);
1983
2036
  this.emitWorkbenchEvent('conversation', {
1984
2037
  event: {
1985
- runId: occurrenceId,
2038
+ runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
1986
2039
  sequence: 1,
1987
2040
  eventType: 'notification',
1988
2041
  payload: {
1989
2042
  targetType: 'WORKSPACE_SESSION',
1990
- userId: task.ownerUserId,
2043
+ userId: recipient,
1991
2044
  workspaceId: task.workspaceId,
1992
2045
  sessionId: task.sessionId,
1993
2046
  sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionId,
1994
- runId: occurrenceId,
1995
- notificationId: `schedule:${task.id}:${occurrenceId}`,
1996
- kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
1997
- code,
2047
+ runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
2048
+ notificationId: event.kind === 'SCHEDULE_TERMINAL'
2049
+ ? `schedule-terminal:${task.id}:${occurrenceId}`
2050
+ : `schedule:${task.id}:${occurrenceId}`,
2051
+ ...event,
1998
2052
  occurredAt
1999
2053
  },
2000
2054
  createdAt: occurredAt
package/dist/database.js CHANGED
@@ -6,9 +6,14 @@ import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
6
6
  import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
7
7
  import { assertNodeSchemaV13, NODE_SCHEMA_V13, NODE_SCHEMA_VERSION_V13 } from './migrations/v013.js';
8
8
  import { NODE_SCHEMA_MIGRATION_V14 } from './migrations/v014.js';
9
+ import { NODE_SCHEMA_MIGRATION_V15 } from './migrations/v015.js';
9
10
  // Future released migrations must be contiguous, immutable and begin at v14; Demo is excluded.
10
- export const NODE_SCHEMA_MIGRATIONS = [NODE_SCHEMA_MIGRATION_V14];
11
+ export const NODE_SCHEMA_MIGRATIONS = [
12
+ NODE_SCHEMA_MIGRATION_V14,
13
+ NODE_SCHEMA_MIGRATION_V15
14
+ ];
11
15
  const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
16
+ const OBSOLETE_NODE_SCHEMA_V16 = 16;
12
17
  export class NodeDatabase {
13
18
  now;
14
19
  raw;
@@ -392,6 +397,13 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
392
397
  const migratedVersions = database
393
398
  .prepare('SELECT version FROM node_schema_migrations ORDER BY version')
394
399
  .all().map((row) => numberValue(row, 'version'));
400
+ if (latest === NODE_SCHEMA_MIGRATION_V15.version &&
401
+ migratedVersions.length === 4 &&
402
+ migratedVersions.every((version, index) => version === NODE_SCHEMA_VERSION_V13 + index) &&
403
+ migratedVersions.at(-1) === OBSOLETE_NODE_SCHEMA_V16) {
404
+ downgradeLegacyRecipientSchema(database);
405
+ migratedVersions.pop();
406
+ }
395
407
  const current = migratedVersions.at(-1);
396
408
  if (current === undefined ||
397
409
  current < NODE_SCHEMA_VERSION_V13 ||
@@ -421,6 +433,49 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
421
433
  if (catalog.length > 0)
422
434
  catalog[catalog.length - 1].assert(database);
423
435
  }
436
+ function downgradeLegacyRecipientSchema(database) {
437
+ const reference = new DatabaseSync(':memory:');
438
+ try {
439
+ migrate(reference, [NODE_SCHEMA_MIGRATION_V14]);
440
+ reference.exec(`ALTER TABLE scheduled_session_tasks
441
+ ADD COLUMN notify_user_id INTEGER NOT NULL DEFAULT 1 CHECK (notify_user_id > 0);
442
+ ${NODE_SCHEMA_MIGRATION_V15.sql}`);
443
+ const definitions = (source) => new Map(source
444
+ .prepare('SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY type,name')
445
+ .all()
446
+ .map((row) => {
447
+ const type = row['type'];
448
+ const name = row['name'];
449
+ const sql = row['sql'];
450
+ if (typeof type !== 'string' || typeof name !== 'string' || typeof sql !== 'string')
451
+ throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
452
+ return [`${type}:${name}`, sql.replace(/\s+/gu, ' ').trim()];
453
+ }));
454
+ const expected = definitions(reference);
455
+ const actual = definitions(database);
456
+ if (expected.size !== actual.size ||
457
+ [...expected].some(([name, sql]) => actual.get(name) !== sql))
458
+ throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
459
+ }
460
+ finally {
461
+ reference.close();
462
+ }
463
+ database.exec('BEGIN IMMEDIATE');
464
+ try {
465
+ if (database.prepare('SELECT 1 FROM scheduled_session_tasks LIMIT 1').get() !== undefined)
466
+ throw new Error('NODE_DATABASE_DOWNGRADE_UNSAFE');
467
+ database.exec('ALTER TABLE scheduled_session_tasks DROP COLUMN notify_user_id');
468
+ NODE_SCHEMA_MIGRATION_V15.assert(database);
469
+ database
470
+ .prepare('DELETE FROM node_schema_migrations WHERE version=?')
471
+ .run(OBSOLETE_NODE_SCHEMA_V16);
472
+ database.exec('COMMIT');
473
+ }
474
+ catch (error) {
475
+ database.exec('ROLLBACK');
476
+ throw error;
477
+ }
478
+ }
424
479
  function mcpInstallation(row) {
425
480
  const runtime = mcpRuntimeDescriptorSchema.parse(JSON.parse(stringValue(row, 'runtime')));
426
481
  const configuration = mcpInstallationConfigurationSchema.parse(JSON.parse(stringValue(row, 'configuration')));
@@ -0,0 +1,12 @@
1
+ import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
2
+ export const NODE_SCHEMA_MIGRATION_V15 = {
3
+ version: 15,
4
+ sql: `ALTER TABLE scheduled_session_tasks
5
+ ADD COLUMN notify_on_terminal INTEGER NOT NULL DEFAULT 0 CHECK (notify_on_terminal IN (0,1));`,
6
+ assert(database) {
7
+ NODE_SCHEMA_MIGRATION_V14.assert(database);
8
+ const columns = new Set(database.prepare('PRAGMA table_info(scheduled_session_tasks)').all().map((column) => column.name));
9
+ if (!columns.has('notify_on_terminal') || columns.has('notify_user_id'))
10
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID');
11
+ }
12
+ };
@@ -173,7 +173,9 @@ function resolvedMcpServers(raw, secrets) {
173
173
  const record = value;
174
174
  if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
175
175
  continue;
176
- if ((record['__marGoal'] === true || record['__marScheduled'] === true) &&
176
+ if ((record['__marGoal'] === true ||
177
+ record['__marScheduled'] === true ||
178
+ record['__marMail'] === true) &&
177
179
  typeof record['command'] === 'string' &&
178
180
  Array.isArray(record['args']) &&
179
181
  record['args'].every((value) => typeof value === 'string') &&
@@ -191,11 +193,16 @@ function resolvedMcpServers(raw, secrets) {
191
193
  MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
192
194
  MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
193
195
  }
194
- : {
195
- MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
196
- MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
197
- MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
198
- }
196
+ : record['__marScheduled'] === true
197
+ ? {
198
+ MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
199
+ MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
200
+ MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
201
+ }
202
+ : {
203
+ MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
204
+ MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
205
+ }
199
206
  });
200
207
  continue;
201
208
  }