@myagentroam/node 0.9.171 → 0.9.173
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 +1 -0
- package/dist/database-relations.js +42 -0
- package/dist/database.js +108 -45
- package/dist/migrations/v016.js +388 -0
- package/dist/runner/mar-agent/compact-extensions.js +37 -3
- package/dist/service/conversation-history-service.js +10 -1
- package/dist/service/hosted-conversation-store.js +24 -10
- package/dist/service/scheduled-session-store.js +103 -51
- package/dist/terminal.js +10 -0
- package/package.json +3 -3
package/dist/connector.js
CHANGED
|
@@ -1399,6 +1399,7 @@ export class NodeConnector {
|
|
|
1399
1399
|
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
1400
1400
|
return this.config;
|
|
1401
1401
|
}, this.runtime, (workspaceId) => {
|
|
1402
|
+
this.terminalManager.removeWorkspaceAssociation(workspaceId);
|
|
1402
1403
|
this.workspaceFileService.dispose(workspaceId);
|
|
1403
1404
|
this.workspaceCoordinator.invalidateChanges(workspaceId);
|
|
1404
1405
|
this.workspaceSessionService.invalidate(workspaceId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function assertNodeNoForeignKeys(database) {
|
|
2
|
+
const tables = database
|
|
3
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
4
|
+
.all();
|
|
5
|
+
for (const { name } of tables) {
|
|
6
|
+
if (database.prepare('SELECT 1 FROM pragma_foreign_key_list(?) LIMIT 1').get(name) !== undefined)
|
|
7
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:foreign_keys:${name}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Validate persisted Node associations independently of physical foreign keys. */
|
|
11
|
+
export function assertNodeRelations(database) {
|
|
12
|
+
const checks = {
|
|
13
|
+
agent_sessions: `SELECT 1 FROM agent_sessions child LEFT JOIN workspaces parent
|
|
14
|
+
ON parent.id=child.workspace_id WHERE parent.id IS NULL LIMIT 1`,
|
|
15
|
+
terminal_sessions: `SELECT 1 FROM terminal_sessions child LEFT JOIN workspaces parent
|
|
16
|
+
ON parent.id=child.workspace_id WHERE child.workspace_id IS NOT NULL AND parent.id IS NULL LIMIT 1`,
|
|
17
|
+
conversation_files: `SELECT 1 FROM conversation_files child LEFT JOIN hosted_conversations parent
|
|
18
|
+
ON parent.id=child.conversation_id WHERE parent.id IS NULL LIMIT 1`,
|
|
19
|
+
hosted_conversation_messages: `SELECT 1 FROM hosted_conversation_messages child LEFT JOIN hosted_conversations parent
|
|
20
|
+
ON parent.id=child.conversation_id WHERE parent.id IS NULL LIMIT 1`,
|
|
21
|
+
hosted_conversations: `SELECT 1 FROM hosted_conversations child LEFT JOIN conversation_template_versions parent
|
|
22
|
+
ON parent.id=child.active_version_id WHERE parent.id IS NULL OR parent.template_id<>child.template_id LIMIT 1`,
|
|
23
|
+
conversation_template_publish_jobs: `SELECT 1 FROM conversation_template_publish_jobs child LEFT JOIN conversation_template_versions parent
|
|
24
|
+
ON parent.id=child.version_id WHERE child.version_id IS NOT NULL
|
|
25
|
+
AND (parent.id IS NULL OR parent.template_id<>child.template_id) LIMIT 1`,
|
|
26
|
+
scheduled_session_tasks: `SELECT 1 FROM scheduled_session_tasks child
|
|
27
|
+
LEFT JOIN agent_sessions session ON session.id=child.session_id
|
|
28
|
+
LEFT JOIN workspaces workspace ON workspace.id=child.workspace_id
|
|
29
|
+
WHERE session.id IS NULL OR workspace.id IS NULL OR session.workspace_id<>child.workspace_id
|
|
30
|
+
OR session.runner<>child.runner LIMIT 1`,
|
|
31
|
+
scheduled_session_occurrences: `SELECT 1 FROM scheduled_session_occurrences child LEFT JOIN scheduled_session_tasks parent
|
|
32
|
+
ON parent.id=child.task_id WHERE parent.id IS NULL LIMIT 1`,
|
|
33
|
+
scheduled_session_proposals: `SELECT 1 FROM scheduled_session_proposals child LEFT JOIN scheduled_session_tasks parent
|
|
34
|
+
ON parent.id=child.task_id WHERE parent.id IS NULL LIMIT 1`,
|
|
35
|
+
mcp_installations: `SELECT 1 FROM mcp_installations child LEFT JOIN workspaces parent
|
|
36
|
+
ON parent.id=child.target_id WHERE child.target_kind='WORKSPACE' AND parent.id IS NULL LIMIT 1`
|
|
37
|
+
};
|
|
38
|
+
for (const [table, query] of Object.entries(checks)) {
|
|
39
|
+
if (database.prepare(query).get() !== undefined)
|
|
40
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:relations:${table}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/database.js
CHANGED
|
@@ -7,10 +7,14 @@ import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDe
|
|
|
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
9
|
import { NODE_SCHEMA_MIGRATION_V15 } from './migrations/v015.js';
|
|
10
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './migrations/v016.js';
|
|
11
|
+
import { ScheduledSessionStore } from './service/scheduled-session-store.js';
|
|
12
|
+
import { assertNodeNoForeignKeys, assertNodeRelations } from './database-relations.js';
|
|
10
13
|
// Future released migrations must be contiguous, immutable and begin at v14; Demo is excluded.
|
|
11
14
|
export const NODE_SCHEMA_MIGRATIONS = [
|
|
12
15
|
NODE_SCHEMA_MIGRATION_V14,
|
|
13
|
-
NODE_SCHEMA_MIGRATION_V15
|
|
16
|
+
NODE_SCHEMA_MIGRATION_V15,
|
|
17
|
+
NODE_SCHEMA_MIGRATION_V16
|
|
14
18
|
];
|
|
15
19
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
16
20
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -24,14 +28,14 @@ export class NodeDatabase {
|
|
|
24
28
|
const releaseMigrationLock = acquireNodeMigrationLock(path);
|
|
25
29
|
try {
|
|
26
30
|
this.raw = new DatabaseSync(path, {
|
|
27
|
-
enableForeignKeyConstraints:
|
|
31
|
+
enableForeignKeyConstraints: false,
|
|
28
32
|
timeout: 5_000,
|
|
29
33
|
defensive: true
|
|
30
34
|
});
|
|
31
35
|
try {
|
|
32
36
|
// Refuse unsupported databases before changing the on-disk journal mode.
|
|
33
37
|
migrate(this.raw);
|
|
34
|
-
this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA
|
|
38
|
+
this.raw.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
|
|
35
39
|
}
|
|
36
40
|
catch (error) {
|
|
37
41
|
this.raw.close();
|
|
@@ -42,6 +46,18 @@ export class NodeDatabase {
|
|
|
42
46
|
releaseMigrationLock();
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
write(action) {
|
|
50
|
+
this.raw.exec('BEGIN IMMEDIATE');
|
|
51
|
+
try {
|
|
52
|
+
const result = action();
|
|
53
|
+
this.raw.exec('COMMIT');
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.raw.exec('ROLLBACK');
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
45
61
|
close() {
|
|
46
62
|
this.raw.close();
|
|
47
63
|
}
|
|
@@ -78,33 +94,35 @@ export class NodeDatabase {
|
|
|
78
94
|
return rows.map(mcpInstallation);
|
|
79
95
|
}
|
|
80
96
|
saveMcpInstallation(input) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
return this.write(() => {
|
|
98
|
+
const entry = mcpCatalogEntrySchema.parse(input.entry);
|
|
99
|
+
const configuration = mcpInstallationConfigurationSchema.parse(input.configuration);
|
|
100
|
+
const targetId = input.targetKind === 'NODE' ? '' : (input.targetId ?? '');
|
|
101
|
+
if (input.targetKind === 'WORKSPACE' && this.getWorkspace(targetId) === undefined)
|
|
102
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
103
|
+
const installedAt = this.now();
|
|
104
|
+
this.raw
|
|
105
|
+
.prepare(`INSERT INTO mcp_installations
|
|
106
|
+
(target_kind,target_id,catalog_entry_id,server_name,display_name,version,enabled,runtime,configuration,installed_at)
|
|
107
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
108
|
+
ON CONFLICT(target_kind,target_id,server_name) DO UPDATE SET
|
|
109
|
+
catalog_entry_id=excluded.catalog_entry_id,display_name=excluded.display_name,
|
|
110
|
+
version=excluded.version,enabled=excluded.enabled,runtime=excluded.runtime,
|
|
111
|
+
configuration=excluded.configuration,installed_at=excluded.installed_at`)
|
|
112
|
+
.run(input.targetKind, targetId, entry.id, entry.serverName, entry.displayName, entry.version, input.enabled ? 1 : 0, JSON.stringify(entry.runtime), JSON.stringify(configuration), installedAt);
|
|
113
|
+
return {
|
|
114
|
+
targetKind: input.targetKind,
|
|
115
|
+
targetId,
|
|
116
|
+
catalogEntryId: entry.id,
|
|
117
|
+
serverName: entry.serverName,
|
|
118
|
+
displayName: entry.displayName,
|
|
119
|
+
version: entry.version,
|
|
120
|
+
enabled: input.enabled,
|
|
121
|
+
runtime: entry.runtime,
|
|
122
|
+
configuration,
|
|
123
|
+
installedAt
|
|
124
|
+
};
|
|
125
|
+
});
|
|
108
126
|
}
|
|
109
127
|
removeMcpInstallation(targetKind, targetId, serverName) {
|
|
110
128
|
return (Number(this.raw
|
|
@@ -125,15 +143,19 @@ export class NodeDatabase {
|
|
|
125
143
|
* the Node-local bounded replay buffer and must never be passed here.
|
|
126
144
|
*/
|
|
127
145
|
saveTerminalSession(input) {
|
|
128
|
-
this.
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
return this.write(() => {
|
|
147
|
+
if (input.workspaceId !== null && this.getWorkspace(input.workspaceId) === undefined)
|
|
148
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
149
|
+
this.raw
|
|
150
|
+
.prepare(`INSERT INTO terminal_sessions
|
|
151
|
+
(id, workspace_id, profile_id, state, exit_code, rows, cols, latest_sequence, created_at, updated_at)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
153
|
+
ON CONFLICT(id) DO UPDATE SET workspace_id = excluded.workspace_id, profile_id = excluded.profile_id, state = excluded.state,
|
|
154
|
+
exit_code = excluded.exit_code, rows = excluded.rows, cols = excluded.cols,
|
|
155
|
+
latest_sequence = excluded.latest_sequence, updated_at = excluded.updated_at`)
|
|
156
|
+
.run(input.id, input.workspaceId, input.profileId, input.state, input.exitCode, input.rows, input.cols, input.latestSequence, input.createdAt, input.updatedAt);
|
|
157
|
+
return this.getTerminalSession(input.id);
|
|
158
|
+
});
|
|
137
159
|
}
|
|
138
160
|
getTerminalSession(id) {
|
|
139
161
|
const row = this.raw
|
|
@@ -203,14 +225,16 @@ export class NodeDatabase {
|
|
|
203
225
|
const current = this.getWorkspace(id);
|
|
204
226
|
if (current === undefined)
|
|
205
227
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
206
|
-
this.raw.exec('BEGIN');
|
|
228
|
+
this.raw.exec('BEGIN IMMEDIATE');
|
|
207
229
|
try {
|
|
208
|
-
|
|
209
|
-
// record itself never represents a filesystem deletion.
|
|
230
|
+
new ScheduledSessionStore(this.raw).removeForWorkspace(id);
|
|
210
231
|
this.raw.prepare('DELETE FROM agent_sessions WHERE workspace_id = ?').run(id);
|
|
211
232
|
this.raw
|
|
212
233
|
.prepare("DELETE FROM mcp_installations WHERE target_kind='WORKSPACE' AND target_id = ?")
|
|
213
234
|
.run(id);
|
|
235
|
+
this.raw
|
|
236
|
+
.prepare('UPDATE terminal_sessions SET workspace_id=NULL WHERE workspace_id=?')
|
|
237
|
+
.run(id);
|
|
214
238
|
this.raw.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
|
|
215
239
|
this.raw.exec('COMMIT');
|
|
216
240
|
return current;
|
|
@@ -267,6 +291,16 @@ export class NodeDatabase {
|
|
|
267
291
|
});
|
|
268
292
|
this.raw.exec('BEGIN IMMEDIATE');
|
|
269
293
|
try {
|
|
294
|
+
if (this.getWorkspace(input.workspaceId) === undefined)
|
|
295
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
296
|
+
const scheduled = this.raw
|
|
297
|
+
.prepare('SELECT workspace_id,runner FROM scheduled_session_tasks WHERE session_id=?')
|
|
298
|
+
.get(input.externalSessionId);
|
|
299
|
+
if (scheduled !== undefined &&
|
|
300
|
+
(scheduled['workspace_id'] !== input.workspaceId || scheduled['runner'] !== input.runner))
|
|
301
|
+
throw Object.assign(new Error('SCHEDULE_SESSION_MISMATCH'), {
|
|
302
|
+
publicMessage: "The scheduled task must use its session's Workspace and Runner."
|
|
303
|
+
});
|
|
270
304
|
if (pinAtTop)
|
|
271
305
|
this.raw
|
|
272
306
|
.prepare('UPDATE agent_sessions SET pin_order = pin_order + 1 WHERE workspace_id = ? AND pin_order IS NOT NULL')
|
|
@@ -339,7 +373,18 @@ export class NodeDatabase {
|
|
|
339
373
|
}
|
|
340
374
|
}
|
|
341
375
|
deleteSessionRecord(sessionId) {
|
|
342
|
-
|
|
376
|
+
this.raw.exec('BEGIN IMMEDIATE');
|
|
377
|
+
try {
|
|
378
|
+
new ScheduledSessionStore(this.raw).removeForSession(sessionId);
|
|
379
|
+
const removed = Number(this.raw.prepare('DELETE FROM agent_sessions WHERE id = ?').run(sessionId).changes) >
|
|
380
|
+
0;
|
|
381
|
+
this.raw.exec('COMMIT');
|
|
382
|
+
return removed;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
this.raw.exec('ROLLBACK');
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
343
388
|
}
|
|
344
389
|
}
|
|
345
390
|
export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
@@ -397,10 +442,12 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
397
442
|
const migratedVersions = database
|
|
398
443
|
.prepare('SELECT version FROM node_schema_migrations ORDER BY version')
|
|
399
444
|
.all().map((row) => numberValue(row, 'version'));
|
|
400
|
-
if (latest
|
|
445
|
+
if (latest >= NODE_SCHEMA_MIGRATION_V15.version &&
|
|
401
446
|
migratedVersions.length === 4 &&
|
|
402
447
|
migratedVersions.every((version, index) => version === NODE_SCHEMA_VERSION_V13 + index) &&
|
|
403
|
-
migratedVersions.at(-1) === OBSOLETE_NODE_SCHEMA_V16
|
|
448
|
+
migratedVersions.at(-1) === OBSOLETE_NODE_SCHEMA_V16 &&
|
|
449
|
+
database.prepare('PRAGMA table_info(scheduled_session_tasks)').all().some((column) => column.name === 'notify_user_id') &&
|
|
450
|
+
database.prepare('PRAGMA foreign_key_list(scheduled_session_tasks)').all().length !== 0) {
|
|
404
451
|
downgradeLegacyRecipientSchema(database);
|
|
405
452
|
migratedVersions.pop();
|
|
406
453
|
}
|
|
@@ -416,10 +463,18 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
416
463
|
for (const entry of catalog) {
|
|
417
464
|
if (entry.version <= current)
|
|
418
465
|
continue;
|
|
466
|
+
const foreignKeys = database.prepare('PRAGMA foreign_keys').get()?.['foreign_keys'];
|
|
467
|
+
if (entry.rebuildTables)
|
|
468
|
+
database.exec('PRAGMA foreign_keys=OFF');
|
|
419
469
|
database.exec('BEGIN IMMEDIATE');
|
|
420
470
|
try {
|
|
471
|
+
entry.preflight?.(database);
|
|
421
472
|
database.exec(entry.sql);
|
|
422
473
|
entry.assert(database);
|
|
474
|
+
if (entry.version >= NODE_SCHEMA_MIGRATION_V16.version) {
|
|
475
|
+
assertNodeNoForeignKeys(database);
|
|
476
|
+
assertNodeRelations(database);
|
|
477
|
+
}
|
|
423
478
|
database
|
|
424
479
|
.prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
|
|
425
480
|
.run(entry.version, Date.now());
|
|
@@ -429,9 +484,17 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
429
484
|
database.exec('ROLLBACK');
|
|
430
485
|
throw error;
|
|
431
486
|
}
|
|
487
|
+
finally {
|
|
488
|
+
if (entry.rebuildTables && foreignKeys === 1)
|
|
489
|
+
database.exec('PRAGMA foreign_keys=ON');
|
|
490
|
+
}
|
|
432
491
|
}
|
|
433
492
|
if (catalog.length > 0)
|
|
434
493
|
catalog[catalog.length - 1].assert(database);
|
|
494
|
+
if (latest >= NODE_SCHEMA_MIGRATION_V16.version) {
|
|
495
|
+
assertNodeNoForeignKeys(database);
|
|
496
|
+
assertNodeRelations(database);
|
|
497
|
+
}
|
|
435
498
|
}
|
|
436
499
|
function downgradeLegacyRecipientSchema(database) {
|
|
437
500
|
const reference = new DatabaseSync(':memory:');
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { assertNodeRelations } from '../database-relations.js';
|
|
3
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
6
|
+
const obsoleteTables = [
|
|
7
|
+
'share_sessions',
|
|
8
|
+
'share_invitations',
|
|
9
|
+
'share_participants',
|
|
10
|
+
'worktree_snapshots',
|
|
11
|
+
'worktree_shares'
|
|
12
|
+
];
|
|
13
|
+
const currentTables = [
|
|
14
|
+
'agent_sessions',
|
|
15
|
+
'conversation_files',
|
|
16
|
+
'conversation_template_publish_jobs',
|
|
17
|
+
'conversation_template_versions',
|
|
18
|
+
'hosted_conversation_messages',
|
|
19
|
+
'hosted_conversations',
|
|
20
|
+
'mcp_installations',
|
|
21
|
+
'node_schema_migrations',
|
|
22
|
+
'runner_defaults',
|
|
23
|
+
'scheduled_session_occurrences',
|
|
24
|
+
'scheduled_session_proposals',
|
|
25
|
+
'scheduled_session_tasks',
|
|
26
|
+
'terminal_sessions',
|
|
27
|
+
'workspaces'
|
|
28
|
+
];
|
|
29
|
+
const rebuiltColumns = {
|
|
30
|
+
agent_sessions: ['id', 'workspace_id', 'runner', 'pin_order', 'metadata'],
|
|
31
|
+
conversation_files: [
|
|
32
|
+
'id',
|
|
33
|
+
'conversation_id',
|
|
34
|
+
'category',
|
|
35
|
+
'relative_path',
|
|
36
|
+
'display_name',
|
|
37
|
+
'mime_type',
|
|
38
|
+
'size',
|
|
39
|
+
'source',
|
|
40
|
+
'turn_id',
|
|
41
|
+
'item_id',
|
|
42
|
+
'created_at',
|
|
43
|
+
'updated_at'
|
|
44
|
+
],
|
|
45
|
+
conversation_template_publish_jobs: [
|
|
46
|
+
'id',
|
|
47
|
+
'template_id',
|
|
48
|
+
'draft_revision',
|
|
49
|
+
'stage',
|
|
50
|
+
'status',
|
|
51
|
+
'version_id',
|
|
52
|
+
'error_code',
|
|
53
|
+
'error_summary',
|
|
54
|
+
'requested_by_user_id',
|
|
55
|
+
'draft',
|
|
56
|
+
'created_at',
|
|
57
|
+
'updated_at'
|
|
58
|
+
],
|
|
59
|
+
hosted_conversation_messages: [
|
|
60
|
+
'conversation_id',
|
|
61
|
+
'client_message_id',
|
|
62
|
+
'run_id',
|
|
63
|
+
'status',
|
|
64
|
+
'created_at',
|
|
65
|
+
'updated_at',
|
|
66
|
+
'user_text_present'
|
|
67
|
+
],
|
|
68
|
+
hosted_conversations: [
|
|
69
|
+
'id',
|
|
70
|
+
'user_storage_key',
|
|
71
|
+
'conversation_storage_key',
|
|
72
|
+
'client_request_id',
|
|
73
|
+
'node_id',
|
|
74
|
+
'session_id',
|
|
75
|
+
'template_id',
|
|
76
|
+
'template_name',
|
|
77
|
+
'active_version_id',
|
|
78
|
+
'title',
|
|
79
|
+
'status',
|
|
80
|
+
'model_id',
|
|
81
|
+
'template_deleted',
|
|
82
|
+
'used_bytes',
|
|
83
|
+
'max_bytes',
|
|
84
|
+
'created_at',
|
|
85
|
+
'updated_at',
|
|
86
|
+
'last_activity_at',
|
|
87
|
+
'pinned_at',
|
|
88
|
+
'title_source'
|
|
89
|
+
],
|
|
90
|
+
scheduled_session_occurrences: [
|
|
91
|
+
'id',
|
|
92
|
+
'task_id',
|
|
93
|
+
'due_at',
|
|
94
|
+
'status',
|
|
95
|
+
'code',
|
|
96
|
+
'run_id',
|
|
97
|
+
'created_at',
|
|
98
|
+
'updated_at'
|
|
99
|
+
],
|
|
100
|
+
scheduled_session_proposals: [
|
|
101
|
+
'id',
|
|
102
|
+
'task_id',
|
|
103
|
+
'proposed_by_user_id',
|
|
104
|
+
'base_revision',
|
|
105
|
+
'objective',
|
|
106
|
+
'cron',
|
|
107
|
+
'timezone',
|
|
108
|
+
'status',
|
|
109
|
+
'created_at',
|
|
110
|
+
'resolved_at'
|
|
111
|
+
],
|
|
112
|
+
scheduled_session_tasks: [
|
|
113
|
+
'id',
|
|
114
|
+
'session_id',
|
|
115
|
+
'workspace_id',
|
|
116
|
+
'runner',
|
|
117
|
+
'owner_user_id',
|
|
118
|
+
'objective',
|
|
119
|
+
'cron',
|
|
120
|
+
'timezone',
|
|
121
|
+
'enabled',
|
|
122
|
+
'revision',
|
|
123
|
+
'next_run_at',
|
|
124
|
+
'created_at',
|
|
125
|
+
'updated_at',
|
|
126
|
+
'notify_on_terminal'
|
|
127
|
+
],
|
|
128
|
+
terminal_sessions: [
|
|
129
|
+
'id',
|
|
130
|
+
'workspace_id',
|
|
131
|
+
'profile_id',
|
|
132
|
+
'state',
|
|
133
|
+
'exit_code',
|
|
134
|
+
'rows',
|
|
135
|
+
'cols',
|
|
136
|
+
'latest_sequence',
|
|
137
|
+
'created_at',
|
|
138
|
+
'updated_at'
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
export const NODE_SCHEMA_MIGRATION_V16 = {
|
|
142
|
+
version: 16,
|
|
143
|
+
rebuildTables: true,
|
|
144
|
+
preflight(database) {
|
|
145
|
+
const tables = database
|
|
146
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
147
|
+
.all();
|
|
148
|
+
for (const { name } of tables) {
|
|
149
|
+
if (obsoleteTables.some((table) => table === name)) {
|
|
150
|
+
if (database.prepare(`SELECT 1 FROM "${name}" LIMIT 1`).get() !== undefined)
|
|
151
|
+
throw new Error(`NODE_DATABASE_OBSOLETE_TABLE_NOT_EMPTY:${name}`);
|
|
152
|
+
}
|
|
153
|
+
else if (!currentTables.some((table) => table === name)) {
|
|
154
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:table:${name}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [table, expected] of Object.entries(rebuiltColumns)) {
|
|
158
|
+
const actual = database.prepare(`PRAGMA table_info(${table})`).all();
|
|
159
|
+
if (actual.length !== expected.length ||
|
|
160
|
+
actual.some((column) => !expected.includes(column.name)))
|
|
161
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${table}`);
|
|
162
|
+
}
|
|
163
|
+
if (database.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' LIMIT 1").get() !==
|
|
164
|
+
undefined)
|
|
165
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:triggers');
|
|
166
|
+
const reference = new DatabaseSync(':memory:');
|
|
167
|
+
try {
|
|
168
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
169
|
+
reference.exec(NODE_SCHEMA_MIGRATION_V14.sql);
|
|
170
|
+
reference.exec(NODE_SCHEMA_MIGRATION_V15.sql);
|
|
171
|
+
const normalize = (sql) => sql
|
|
172
|
+
.replace(/\s+REFERENCES\s+\w+\([^)]*\)(?:\s+ON DELETE\s+(?:CASCADE|SET NULL))?/giu, '')
|
|
173
|
+
.replace(/"(\w+)"/gu, '$1')
|
|
174
|
+
.replace(/\s+/gu, ' ')
|
|
175
|
+
.replace(/\s*([(),])\s*/gu, '$1')
|
|
176
|
+
.trim();
|
|
177
|
+
const query = "SELECT type,name,tbl_name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name";
|
|
178
|
+
const expected = reference.prepare(query).all();
|
|
179
|
+
const actual = database.prepare(query).all().filter((entry) => !obsoleteTables.some((table) => table === entry.tbl_name));
|
|
180
|
+
const definition = (entry) => {
|
|
181
|
+
const sql = normalize(entry.sql);
|
|
182
|
+
// Released upgrades appended this nullable column; new installs declared it second.
|
|
183
|
+
return entry.name === 'terminal_sessions'
|
|
184
|
+
? sql.replace(/(?:,workspace_id TEXT|workspace_id TEXT,)/u, '')
|
|
185
|
+
: sql;
|
|
186
|
+
};
|
|
187
|
+
if (actual.length !== expected.length ||
|
|
188
|
+
expected.some((entry, index) => entry.type !== actual[index]?.type ||
|
|
189
|
+
entry.name !== actual[index]?.name ||
|
|
190
|
+
definition(entry) !== definition(actual[index])))
|
|
191
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:definitions');
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
reference.close();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
sql: `DROP TABLE IF EXISTS share_sessions;
|
|
198
|
+
DROP TABLE IF EXISTS share_invitations;
|
|
199
|
+
DROP TABLE IF EXISTS share_participants;
|
|
200
|
+
DROP TABLE IF EXISTS worktree_snapshots;
|
|
201
|
+
DROP TABLE IF EXISTS worktree_shares;
|
|
202
|
+
CREATE TABLE _node_v16_agent_sessions (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
workspace_id TEXT NOT NULL,
|
|
205
|
+
runner TEXT NOT NULL CHECK (runner IN ('mar-agent', 'codex', 'claude-code', 'opencode')),
|
|
206
|
+
pin_order INTEGER CHECK (pin_order IS NULL OR pin_order >= 0),
|
|
207
|
+
metadata TEXT NOT NULL CHECK (json_valid(metadata))
|
|
208
|
+
) STRICT;
|
|
209
|
+
INSERT INTO _node_v16_agent_sessions (id,workspace_id,runner,pin_order,metadata) SELECT id,workspace_id,runner,pin_order,metadata FROM agent_sessions;
|
|
210
|
+
DROP TABLE agent_sessions;
|
|
211
|
+
ALTER TABLE _node_v16_agent_sessions RENAME TO agent_sessions;
|
|
212
|
+
CREATE TABLE _node_v16_conversation_files (
|
|
213
|
+
id TEXT PRIMARY KEY,
|
|
214
|
+
conversation_id TEXT NOT NULL,
|
|
215
|
+
category TEXT NOT NULL CHECK (category IN ('UPLOAD','OUTPUT')),
|
|
216
|
+
relative_path TEXT NOT NULL,
|
|
217
|
+
display_name TEXT NOT NULL,
|
|
218
|
+
mime_type TEXT NOT NULL,
|
|
219
|
+
size INTEGER NOT NULL CHECK (size >= 0),
|
|
220
|
+
source TEXT NOT NULL CHECK (source IN ('USER','AGENT','SYSTEM')),
|
|
221
|
+
turn_id TEXT,
|
|
222
|
+
item_id TEXT,
|
|
223
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
224
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
225
|
+
UNIQUE (conversation_id, category, relative_path)
|
|
226
|
+
) STRICT;
|
|
227
|
+
INSERT INTO _node_v16_conversation_files (id,conversation_id,category,relative_path,display_name,mime_type,size,source,turn_id,item_id,created_at,updated_at) SELECT id,conversation_id,category,relative_path,display_name,mime_type,size,source,turn_id,item_id,created_at,updated_at FROM conversation_files;
|
|
228
|
+
DROP TABLE conversation_files;
|
|
229
|
+
ALTER TABLE _node_v16_conversation_files RENAME TO conversation_files;
|
|
230
|
+
CREATE INDEX conversation_files_conversation_updated
|
|
231
|
+
ON conversation_files(conversation_id, updated_at DESC, id);
|
|
232
|
+
CREATE TABLE _node_v16_conversation_template_publish_jobs (
|
|
233
|
+
id TEXT PRIMARY KEY,
|
|
234
|
+
template_id TEXT NOT NULL,
|
|
235
|
+
draft_revision INTEGER NOT NULL CHECK (draft_revision > 0),
|
|
236
|
+
stage TEXT NOT NULL CHECK (
|
|
237
|
+
stage IN ('VALIDATING','ENUMERATING','COPYING','SCANNING','MANIFEST','SEALED','REGISTERED')
|
|
238
|
+
),
|
|
239
|
+
status TEXT NOT NULL CHECK (status IN ('QUEUED','RUNNING','SUCCEEDED','FAILED')),
|
|
240
|
+
version_id TEXT,
|
|
241
|
+
error_code TEXT,
|
|
242
|
+
error_summary TEXT,
|
|
243
|
+
requested_by_user_id INTEGER NOT NULL CHECK (requested_by_user_id > 0),
|
|
244
|
+
draft TEXT NOT NULL CHECK (json_valid(draft)),
|
|
245
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
246
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
|
|
247
|
+
) STRICT;
|
|
248
|
+
INSERT INTO _node_v16_conversation_template_publish_jobs (id,template_id,draft_revision,stage,status,version_id,error_code,error_summary,requested_by_user_id,draft,created_at,updated_at) SELECT id,template_id,draft_revision,stage,status,version_id,error_code,error_summary,requested_by_user_id,draft,created_at,updated_at FROM conversation_template_publish_jobs;
|
|
249
|
+
DROP TABLE conversation_template_publish_jobs;
|
|
250
|
+
ALTER TABLE _node_v16_conversation_template_publish_jobs RENAME TO conversation_template_publish_jobs;
|
|
251
|
+
CREATE INDEX conversation_template_publish_jobs_status
|
|
252
|
+
ON conversation_template_publish_jobs(status, updated_at);
|
|
253
|
+
CREATE INDEX conversation_template_publish_jobs_template
|
|
254
|
+
ON conversation_template_publish_jobs(template_id, created_at, id);
|
|
255
|
+
CREATE TABLE _node_v16_hosted_conversation_messages (
|
|
256
|
+
conversation_id TEXT NOT NULL,
|
|
257
|
+
client_message_id TEXT NOT NULL,
|
|
258
|
+
run_id TEXT NOT NULL,
|
|
259
|
+
status TEXT NOT NULL CHECK (status IN ('RUNNING','SUCCEEDED','FAILED','INTERRUPTED')),
|
|
260
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
261
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0), user_text_present INTEGER NOT NULL DEFAULT 1
|
|
262
|
+
CHECK (user_text_present IN (0,1)),
|
|
263
|
+
PRIMARY KEY (conversation_id, client_message_id)
|
|
264
|
+
) STRICT;
|
|
265
|
+
INSERT INTO _node_v16_hosted_conversation_messages (conversation_id,client_message_id,run_id,status,created_at,updated_at,user_text_present) SELECT conversation_id,client_message_id,run_id,status,created_at,updated_at,user_text_present FROM hosted_conversation_messages;
|
|
266
|
+
DROP TABLE hosted_conversation_messages;
|
|
267
|
+
ALTER TABLE _node_v16_hosted_conversation_messages RENAME TO hosted_conversation_messages;
|
|
268
|
+
CREATE TABLE _node_v16_hosted_conversations (
|
|
269
|
+
id TEXT PRIMARY KEY,
|
|
270
|
+
user_storage_key TEXT NOT NULL,
|
|
271
|
+
conversation_storage_key TEXT NOT NULL,
|
|
272
|
+
client_request_id TEXT,
|
|
273
|
+
node_id TEXT NOT NULL,
|
|
274
|
+
session_id TEXT NOT NULL,
|
|
275
|
+
template_id TEXT NOT NULL,
|
|
276
|
+
template_name TEXT NOT NULL,
|
|
277
|
+
active_version_id TEXT NOT NULL,
|
|
278
|
+
title TEXT NOT NULL,
|
|
279
|
+
status TEXT NOT NULL CHECK (status IN ('ACTIVE')),
|
|
280
|
+
model_id TEXT NOT NULL,
|
|
281
|
+
template_deleted INTEGER NOT NULL DEFAULT 0 CHECK (template_deleted IN (0,1)),
|
|
282
|
+
used_bytes INTEGER NOT NULL DEFAULT 0 CHECK (used_bytes >= 0),
|
|
283
|
+
max_bytes INTEGER NOT NULL CHECK (max_bytes > 0),
|
|
284
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
285
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
286
|
+
last_activity_at INTEGER NOT NULL CHECK (last_activity_at >= 0),
|
|
287
|
+
pinned_at INTEGER CHECK (pinned_at IS NULL OR pinned_at >= 0),
|
|
288
|
+
title_source TEXT NOT NULL DEFAULT 'CUSTOM'
|
|
289
|
+
CHECK (title_source IN ('DEFAULT','AUTO','CUSTOM')),
|
|
290
|
+
UNIQUE (user_storage_key, conversation_storage_key),
|
|
291
|
+
UNIQUE (user_storage_key, client_request_id)
|
|
292
|
+
) STRICT;
|
|
293
|
+
INSERT INTO _node_v16_hosted_conversations (id,user_storage_key,conversation_storage_key,client_request_id,node_id,session_id,template_id,template_name,active_version_id,title,status,model_id,template_deleted,used_bytes,max_bytes,created_at,updated_at,last_activity_at,pinned_at,title_source) SELECT id,user_storage_key,conversation_storage_key,client_request_id,node_id,session_id,template_id,template_name,active_version_id,title,status,model_id,template_deleted,used_bytes,max_bytes,created_at,updated_at,last_activity_at,pinned_at,title_source FROM hosted_conversations;
|
|
294
|
+
DROP TABLE hosted_conversations;
|
|
295
|
+
ALTER TABLE _node_v16_hosted_conversations RENAME TO hosted_conversations;
|
|
296
|
+
CREATE INDEX hosted_conversations_template
|
|
297
|
+
ON hosted_conversations(template_id, status, id);
|
|
298
|
+
CREATE INDEX hosted_conversations_user_pin_activity
|
|
299
|
+
ON hosted_conversations(
|
|
300
|
+
user_storage_key,
|
|
301
|
+
(pinned_at IS NOT NULL) DESC,
|
|
302
|
+
last_activity_at DESC,
|
|
303
|
+
id DESC
|
|
304
|
+
);
|
|
305
|
+
CREATE TABLE _node_v16_scheduled_session_occurrences (
|
|
306
|
+
id TEXT PRIMARY KEY,
|
|
307
|
+
task_id TEXT NOT NULL,
|
|
308
|
+
due_at INTEGER NOT NULL CHECK (due_at >= 0),
|
|
309
|
+
status TEXT NOT NULL CHECK (status IN ('CLAIMED','SUBMITTED','SUCCEEDED','SKIPPED','FAILED')),
|
|
310
|
+
code TEXT,
|
|
311
|
+
run_id TEXT,
|
|
312
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
313
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
|
|
314
|
+
UNIQUE(task_id, due_at)
|
|
315
|
+
) STRICT;
|
|
316
|
+
INSERT INTO _node_v16_scheduled_session_occurrences (id,task_id,due_at,status,code,run_id,created_at,updated_at) SELECT id,task_id,due_at,status,code,run_id,created_at,updated_at FROM scheduled_session_occurrences;
|
|
317
|
+
DROP TABLE scheduled_session_occurrences;
|
|
318
|
+
ALTER TABLE _node_v16_scheduled_session_occurrences RENAME TO scheduled_session_occurrences;
|
|
319
|
+
CREATE INDEX scheduled_session_occurrences_recent
|
|
320
|
+
ON scheduled_session_occurrences(task_id, due_at DESC);
|
|
321
|
+
CREATE TABLE _node_v16_scheduled_session_proposals (
|
|
322
|
+
id TEXT PRIMARY KEY,
|
|
323
|
+
task_id TEXT NOT NULL,
|
|
324
|
+
proposed_by_user_id INTEGER NOT NULL CHECK (proposed_by_user_id > 0),
|
|
325
|
+
base_revision INTEGER NOT NULL CHECK (base_revision > 0),
|
|
326
|
+
objective TEXT CHECK (objective IS NULL OR length(objective) BETWEEN 1 AND 20000),
|
|
327
|
+
cron TEXT,
|
|
328
|
+
timezone TEXT,
|
|
329
|
+
status TEXT NOT NULL CHECK (status IN ('PENDING','ACCEPTED','REJECTED','STALE')),
|
|
330
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
331
|
+
resolved_at INTEGER
|
|
332
|
+
) STRICT;
|
|
333
|
+
INSERT INTO _node_v16_scheduled_session_proposals (id,task_id,proposed_by_user_id,base_revision,objective,cron,timezone,status,created_at,resolved_at) SELECT id,task_id,proposed_by_user_id,base_revision,objective,cron,timezone,status,created_at,resolved_at FROM scheduled_session_proposals;
|
|
334
|
+
DROP TABLE scheduled_session_proposals;
|
|
335
|
+
ALTER TABLE _node_v16_scheduled_session_proposals RENAME TO scheduled_session_proposals;
|
|
336
|
+
CREATE INDEX scheduled_session_proposals_pending
|
|
337
|
+
ON scheduled_session_proposals(task_id, status, created_at DESC);
|
|
338
|
+
CREATE TABLE _node_v16_scheduled_session_tasks (
|
|
339
|
+
id TEXT PRIMARY KEY,
|
|
340
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
341
|
+
workspace_id TEXT NOT NULL,
|
|
342
|
+
runner TEXT NOT NULL CHECK (runner IN ('mar-agent','codex','claude-code','opencode')),
|
|
343
|
+
owner_user_id INTEGER NOT NULL CHECK (owner_user_id > 0),
|
|
344
|
+
objective TEXT NOT NULL CHECK (length(objective) BETWEEN 1 AND 20000),
|
|
345
|
+
cron TEXT NOT NULL,
|
|
346
|
+
timezone TEXT NOT NULL,
|
|
347
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
|
|
348
|
+
revision INTEGER NOT NULL CHECK (revision > 0),
|
|
349
|
+
next_run_at INTEGER CHECK (next_run_at IS NULL OR next_run_at >= 0),
|
|
350
|
+
created_at INTEGER NOT NULL CHECK (created_at >= 0),
|
|
351
|
+
updated_at INTEGER NOT NULL CHECK (updated_at >= 0)
|
|
352
|
+
, notify_on_terminal INTEGER NOT NULL DEFAULT 0 CHECK (notify_on_terminal IN (0,1))) STRICT;
|
|
353
|
+
INSERT INTO _node_v16_scheduled_session_tasks (id,session_id,workspace_id,runner,owner_user_id,objective,cron,timezone,enabled,revision,next_run_at,created_at,updated_at,notify_on_terminal) SELECT id,session_id,workspace_id,runner,owner_user_id,objective,cron,timezone,enabled,revision,next_run_at,created_at,updated_at,notify_on_terminal FROM scheduled_session_tasks;
|
|
354
|
+
DROP TABLE scheduled_session_tasks;
|
|
355
|
+
ALTER TABLE _node_v16_scheduled_session_tasks RENAME TO scheduled_session_tasks;
|
|
356
|
+
CREATE INDEX scheduled_session_tasks_due
|
|
357
|
+
ON scheduled_session_tasks(enabled, next_run_at);
|
|
358
|
+
CREATE TABLE _node_v16_terminal_sessions (
|
|
359
|
+
id TEXT PRIMARY KEY, workspace_id TEXT,
|
|
360
|
+
profile_id TEXT NOT NULL,
|
|
361
|
+
state TEXT NOT NULL CHECK (state IN ('RUNNING', 'EXITED', 'CLOSED')),
|
|
362
|
+
exit_code INTEGER, rows INTEGER NOT NULL CHECK (rows > 0 AND rows <= 1000),
|
|
363
|
+
cols INTEGER NOT NULL CHECK (cols > 0 AND cols <= 1000),
|
|
364
|
+
latest_sequence INTEGER NOT NULL CHECK (latest_sequence >= 0),
|
|
365
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
|
366
|
+
) STRICT;
|
|
367
|
+
INSERT INTO _node_v16_terminal_sessions (id,workspace_id,profile_id,state,exit_code,rows,cols,latest_sequence,created_at,updated_at) SELECT id,workspace_id,profile_id,state,exit_code,rows,cols,latest_sequence,created_at,updated_at FROM terminal_sessions;
|
|
368
|
+
DROP TABLE terminal_sessions;
|
|
369
|
+
ALTER TABLE _node_v16_terminal_sessions RENAME TO terminal_sessions;`,
|
|
370
|
+
assert(database) {
|
|
371
|
+
NODE_SCHEMA_MIGRATION_V15.assert(database);
|
|
372
|
+
NODE_SCHEMA_MIGRATION_V16.preflight(database);
|
|
373
|
+
const tables = database
|
|
374
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
375
|
+
.all();
|
|
376
|
+
if (tables.length !== currentTables.length ||
|
|
377
|
+
tables.some(({ name }) => !currentTables.some((table) => table === name)))
|
|
378
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:tables');
|
|
379
|
+
for (const { name } of tables) {
|
|
380
|
+
if (database.prepare(`PRAGMA foreign_key_list(${name})`).all().length !== 0)
|
|
381
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:foreign_keys:${name}`);
|
|
382
|
+
}
|
|
383
|
+
if (database.prepare("SELECT 1 FROM sqlite_master WHERE type='trigger' LIMIT 1").get() !==
|
|
384
|
+
undefined)
|
|
385
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:triggers');
|
|
386
|
+
assertNodeRelations(database);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCompactEvidence, countModelTokens, generateModelText } from '@myagentroam/agent';
|
|
1
|
+
import { buildCompactEvidence, countModelTokens, generateModelText, MarAgentError } from '@myagentroam/agent';
|
|
2
2
|
import { marAgentRuntimeCompactConfigurationSchema } from '@myagentroam/protocol';
|
|
3
3
|
/** Resolve optional dedicated credentials in the same factory batch without widening the SDK catalog. */
|
|
4
4
|
export function compactRuntimeConfigurations(models, raw) {
|
|
@@ -58,6 +58,7 @@ export function marAgentCompactExtensionSource(state) {
|
|
|
58
58
|
}
|
|
59
59
|
export async function generateMarAgentCompact(context, selected, signal) {
|
|
60
60
|
signal.throwIfAborted();
|
|
61
|
+
const startedAt = Date.now();
|
|
61
62
|
const instruction = context.getPrompt();
|
|
62
63
|
const overhead = countModelTokens(system) + countModelTokens(instruction) + 128;
|
|
63
64
|
const full = buildCompactEvidence(context.messages, {
|
|
@@ -66,14 +67,39 @@ export async function generateMarAgentCompact(context, selected, signal) {
|
|
|
66
67
|
});
|
|
67
68
|
const window = selected.model.contextWindowTokens;
|
|
68
69
|
const budget = Math.floor(window * 0.9) - overhead;
|
|
69
|
-
|
|
70
|
+
let modelRequests = 0;
|
|
71
|
+
let inputTokensEstimateAfter = overhead;
|
|
72
|
+
const report = async (state, reason, errorCode) => {
|
|
73
|
+
try {
|
|
74
|
+
await context.reportDiagnostic?.({
|
|
75
|
+
modelId: selected.model.id,
|
|
76
|
+
providerModelId: selected.model.modelId,
|
|
77
|
+
reasoningEffort: selected.reasoningEffort,
|
|
78
|
+
state,
|
|
79
|
+
inputTokensEstimateBefore: countModelTokens(full) + overhead,
|
|
80
|
+
inputTokensEstimateAfter,
|
|
81
|
+
modelRequests,
|
|
82
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
83
|
+
...(reason === undefined ? {} : { reason }),
|
|
84
|
+
...(errorCode === undefined ? {} : { errorCode })
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Telemetry is best effort and cannot turn a successful generation into a fallback.
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
if (budget <= 0) {
|
|
92
|
+
await report('DECLINED', 'INPUT_BUDGET_EXHAUSTED');
|
|
70
93
|
return undefined;
|
|
94
|
+
}
|
|
71
95
|
const evidence = countModelTokens(full) + overhead <= window
|
|
72
96
|
? full
|
|
73
97
|
: buildCompactEvidence(context.messages, {
|
|
74
98
|
maxTokens: budget,
|
|
75
99
|
...(context.focus === undefined ? {} : { focus: context.focus })
|
|
76
100
|
});
|
|
101
|
+
inputTokensEstimateAfter = countModelTokens(evidence) + overhead;
|
|
102
|
+
await report('STARTED');
|
|
77
103
|
try {
|
|
78
104
|
const result = await generateModelText({
|
|
79
105
|
model: selected.model,
|
|
@@ -84,14 +110,22 @@ export async function generateMarAgentCompact(context, selected, signal) {
|
|
|
84
110
|
{ role: 'user', content: instruction }
|
|
85
111
|
],
|
|
86
112
|
outputLimit: Math.min(selected.model.maxOutputTokens, context.summaryTokenBudget),
|
|
113
|
+
onAttemptDiagnostic: async (diagnostic) => {
|
|
114
|
+
if (diagnostic.state === 'STARTED') {
|
|
115
|
+
modelRequests++;
|
|
116
|
+
await report('STARTED');
|
|
117
|
+
}
|
|
118
|
+
},
|
|
87
119
|
signal
|
|
88
120
|
});
|
|
121
|
+
await report(result ? 'SUCCEEDED' : 'DECLINED', result ? undefined : 'MODEL_INCOMPLETE');
|
|
89
122
|
return result
|
|
90
123
|
? { summary: result.text, ...(result.usage === undefined ? {} : { usage: result.usage }) }
|
|
91
124
|
: undefined;
|
|
92
125
|
}
|
|
93
|
-
catch {
|
|
126
|
+
catch (error) {
|
|
94
127
|
signal.throwIfAborted();
|
|
128
|
+
await report('FAILED', 'MODEL_FAILED', error instanceof MarAgentError ? error.code : 'MAR_AGENT_COMPACT_MODEL_FAILED');
|
|
95
129
|
return undefined;
|
|
96
130
|
}
|
|
97
131
|
}
|
|
@@ -739,7 +739,16 @@ function sameConversationTurn(left, right) {
|
|
|
739
739
|
if (left.id === right.id)
|
|
740
740
|
return true;
|
|
741
741
|
const leftMessageId = conversationClientMessageId(left);
|
|
742
|
-
|
|
742
|
+
if (leftMessageId !== null)
|
|
743
|
+
return leftMessageId === conversationClientMessageId(right);
|
|
744
|
+
if (left.userItemId !== null || right.userItemId !== null)
|
|
745
|
+
return false;
|
|
746
|
+
// Item-only live Turns have no input receipt. Runtime namespaces the stable
|
|
747
|
+
// Runner Item identity by Run, so reconcile it with the unprefixed history Item.
|
|
748
|
+
const [native, local] = left.runId === null ? [left, right] : [right, left];
|
|
749
|
+
if (native.runId !== null || local.runId === null)
|
|
750
|
+
return false;
|
|
751
|
+
return native.items.some((item) => local.items.some((candidate) => candidate.kind === item.kind && candidate.id === `${local.runId}:${item.id}`));
|
|
743
752
|
}
|
|
744
753
|
function terminalConversationTurn(status) {
|
|
745
754
|
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
@@ -3,6 +3,7 @@ import { lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { conversationFileProjectionSchema, hostedConversationManifestSchema, hostedConversationSummarySchema } from '@myagentroam/protocol';
|
|
6
|
+
import { assertNodeRelations } from '../database-relations.js';
|
|
6
7
|
const STORAGE_KEY = /^[A-Za-z0-9][A-Za-z0-9_-]{7,159}$/u;
|
|
7
8
|
const PROTOCOL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
|
|
8
9
|
const CURSOR_BINDING = /^[A-Za-z0-9_-]{43}$/u;
|
|
@@ -194,6 +195,12 @@ export class HostedConversationStore {
|
|
|
194
195
|
database.exec('BEGIN IMMEDIATE');
|
|
195
196
|
try {
|
|
196
197
|
this.adjustVersionReferences(versionReferences(conversation), []);
|
|
198
|
+
database
|
|
199
|
+
.prepare('DELETE FROM conversation_files WHERE conversation_id=?')
|
|
200
|
+
.run(conversation.id);
|
|
201
|
+
database
|
|
202
|
+
.prepare('DELETE FROM hosted_conversation_messages WHERE conversation_id=?')
|
|
203
|
+
.run(conversation.id);
|
|
197
204
|
const deleted = database
|
|
198
205
|
.prepare('DELETE FROM hosted_conversations WHERE id=?')
|
|
199
206
|
.run(conversation.id);
|
|
@@ -626,11 +633,13 @@ export class HostedConversationStore {
|
|
|
626
633
|
.get(conversationId) !== undefined);
|
|
627
634
|
}
|
|
628
635
|
createMessage(record) {
|
|
629
|
-
this.options.database.raw
|
|
636
|
+
const result = this.options.database.raw
|
|
630
637
|
.prepare(`INSERT INTO hosted_conversation_messages
|
|
631
638
|
(conversation_id,client_message_id,run_id,user_text_present,status,created_at,updated_at)
|
|
632
|
-
|
|
633
|
-
.run(record.conversationId, record.clientMessageId, record.runId, record.userTextPresent ? 1 : 0, record.status, record.createdAt, record.updatedAt);
|
|
639
|
+
SELECT ?,?,?,?,?,?,? FROM hosted_conversations WHERE id=?`)
|
|
640
|
+
.run(record.conversationId, record.clientMessageId, record.runId, record.userTextPresent ? 1 : 0, record.status, record.createdAt, record.updatedAt, record.conversationId);
|
|
641
|
+
if (Number(result.changes) !== 1)
|
|
642
|
+
throw new Error('HOSTED_CONVERSATION_NOT_FOUND');
|
|
634
643
|
return record;
|
|
635
644
|
}
|
|
636
645
|
finishMessage(conversationId, clientMessageId, status, updatedAt) {
|
|
@@ -1006,6 +1015,7 @@ export class HostedConversationStore {
|
|
|
1006
1015
|
}
|
|
1007
1016
|
}
|
|
1008
1017
|
insertConversation(manifest) {
|
|
1018
|
+
this.assertConversationVersion(manifest);
|
|
1009
1019
|
this.options.database.raw
|
|
1010
1020
|
.prepare(`INSERT INTO hosted_conversations
|
|
1011
1021
|
(id,user_storage_key,conversation_storage_key,client_request_id,node_id,session_id,
|
|
@@ -1016,6 +1026,7 @@ export class HostedConversationStore {
|
|
|
1016
1026
|
.run(manifest.id, manifest.userStorageKey, manifest.conversationStorageKey, manifest.clientRequestId ?? null, manifest.nodeId, manifest.sessionId, manifest.templateId, manifest.templateName, manifest.activeVersionId, manifest.title, manifest.titleSource, manifest.pinnedAt, manifest.status, manifest.modelId, manifest.templateDeleted ? 1 : 0, manifest.quota.usedBytes, manifest.quota.maxBytes, manifest.createdAt, manifest.updatedAt, manifest.lastActivityAt);
|
|
1017
1027
|
}
|
|
1018
1028
|
updateConversationRow(manifest) {
|
|
1029
|
+
this.assertConversationVersion(manifest);
|
|
1019
1030
|
const result = this.options.database.raw
|
|
1020
1031
|
.prepare(`UPDATE hosted_conversations
|
|
1021
1032
|
SET template_id=?,template_name=?,active_version_id=?,title=?,title_source=?,pinned_at=?,
|
|
@@ -1026,6 +1037,14 @@ export class HostedConversationStore {
|
|
|
1026
1037
|
if (Number(result.changes) !== 1)
|
|
1027
1038
|
throw new Error('HOSTED_CONVERSATION_STORAGE_INVALID');
|
|
1028
1039
|
}
|
|
1040
|
+
assertConversationVersion(manifest) {
|
|
1041
|
+
if (this.options.database.raw
|
|
1042
|
+
.prepare('SELECT 1 FROM conversation_template_versions WHERE id=? AND template_id=?')
|
|
1043
|
+
.get(manifest.activeVersionId, manifest.templateId) === undefined)
|
|
1044
|
+
throw Object.assign(new Error('CONVERSATION_TEMPLATE_VERSION_CORRUPTED'), {
|
|
1045
|
+
publicDetails: 'The active template version does not exist or belongs to another template.'
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1029
1048
|
adjustVersionReferences(previous, next) {
|
|
1030
1049
|
const counts = new Map();
|
|
1031
1050
|
for (const id of previous)
|
|
@@ -1098,6 +1117,7 @@ export class HostedConversationStore {
|
|
|
1098
1117
|
const addReference = database.prepare('UPDATE conversation_template_versions SET reference_count=reference_count+1 WHERE id=?');
|
|
1099
1118
|
for (const item of rebuilt) {
|
|
1100
1119
|
const manifest = item.manifest;
|
|
1120
|
+
this.assertConversationVersion(manifest);
|
|
1101
1121
|
upsert.run(manifest.id, manifest.userStorageKey, manifest.conversationStorageKey, manifest.clientRequestId ?? null, manifest.nodeId, manifest.sessionId, manifest.templateId, manifest.templateName, manifest.activeVersionId, manifest.title, manifest.titleSource, manifest.pinnedAt, manifest.status, manifest.modelId, manifest.templateDeleted ? 1 : 0, manifest.quota.usedBytes, manifest.quota.maxBytes, manifest.createdAt, manifest.updatedAt, manifest.lastActivityAt);
|
|
1102
1122
|
for (const file of item.files)
|
|
1103
1123
|
insertConversationFile(insertFile, file);
|
|
@@ -1108,13 +1128,7 @@ export class HostedConversationStore {
|
|
|
1108
1128
|
.prepare(`UPDATE hosted_conversation_messages
|
|
1109
1129
|
SET status='INTERRUPTED',updated_at=? WHERE status='RUNNING'`)
|
|
1110
1130
|
.run(Date.now());
|
|
1111
|
-
|
|
1112
|
-
'hosted_conversations',
|
|
1113
|
-
'conversation_files',
|
|
1114
|
-
'hosted_conversation_messages'
|
|
1115
|
-
])
|
|
1116
|
-
if (database.prepare(`PRAGMA foreign_key_check(${table})`).get() !== undefined)
|
|
1117
|
-
throw new Error('HOSTED_CONVERSATION_STORAGE_INVALID');
|
|
1131
|
+
assertNodeRelations(database);
|
|
1118
1132
|
database.exec('COMMIT');
|
|
1119
1133
|
}
|
|
1120
1134
|
catch (error) {
|
|
@@ -51,55 +51,100 @@ export class ScheduledSessionStore {
|
|
|
51
51
|
createdAt: now,
|
|
52
52
|
updatedAt: now
|
|
53
53
|
};
|
|
54
|
-
this.
|
|
55
|
-
|
|
54
|
+
return this.write(() => {
|
|
55
|
+
const session = this.database
|
|
56
|
+
.prepare('SELECT workspace_id,runner FROM agent_sessions WHERE id=?')
|
|
57
|
+
.get(task.sessionId);
|
|
58
|
+
if (session === undefined)
|
|
59
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
60
|
+
if (session['workspace_id'] !== task.workspaceId || session['runner'] !== task.runner)
|
|
61
|
+
throw Object.assign(new Error('SCHEDULE_SESSION_MISMATCH'), {
|
|
62
|
+
publicMessage: "The scheduled task must use its session's Workspace and Runner."
|
|
63
|
+
});
|
|
64
|
+
if (this.database.prepare('SELECT 1 FROM workspaces WHERE id=?').get(task.workspaceId) ===
|
|
65
|
+
undefined)
|
|
66
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
67
|
+
this.database
|
|
68
|
+
.prepare(`INSERT INTO scheduled_session_tasks
|
|
56
69
|
(id,session_id,workspace_id,runner,owner_user_id,notify_on_terminal,objective,cron,timezone,enabled,revision,next_run_at,created_at,updated_at)
|
|
57
70
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
.run(task.id, task.sessionId, task.workspaceId, task.runner, task.ownerUserId, task.notifyOnTerminal ? 1 : 0, task.objective, task.cron, task.timezone, task.enabled ? 1 : 0, 1, task.nextRunAt, now, now);
|
|
72
|
+
return task;
|
|
73
|
+
});
|
|
60
74
|
}
|
|
61
75
|
update(sessionId, expectedRevision, patch) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
?
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
return this.write(() => {
|
|
77
|
+
const current = this.get(sessionId);
|
|
78
|
+
if (current === undefined)
|
|
79
|
+
throw new Error('SCHEDULE_NOT_FOUND');
|
|
80
|
+
if (current.revision !== expectedRevision)
|
|
81
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
82
|
+
if (patch.ownerUserId !== undefined &&
|
|
83
|
+
(!Number.isSafeInteger(patch.ownerUserId) || patch.ownerUserId < 1))
|
|
84
|
+
throw new Error('SCHEDULE_OWNER_INVALID');
|
|
85
|
+
const objective = patch.objective ?? current.objective;
|
|
86
|
+
const cron = patch.cron ?? current.cron;
|
|
87
|
+
const timezone = patch.timezone ?? current.timezone;
|
|
88
|
+
validate(objective, cron, timezone);
|
|
89
|
+
const enabled = patch.enabled ?? current.enabled;
|
|
90
|
+
const ownerUserId = patch.ownerUserId ?? current.ownerUserId;
|
|
91
|
+
const nextRunAt = enabled
|
|
92
|
+
? patch.enabled === true || patch.cron !== undefined || patch.timezone !== undefined
|
|
93
|
+
? nextScheduledAt(cron, timezone, this.now())
|
|
94
|
+
: current.nextRunAt
|
|
95
|
+
: null;
|
|
96
|
+
const now = this.now();
|
|
97
|
+
const result = this.database
|
|
98
|
+
.prepare(`UPDATE scheduled_session_tasks SET objective=?,cron=?,timezone=?,enabled=?,owner_user_id=?,notify_on_terminal=?,revision=?,next_run_at=?,updated_at=?
|
|
84
99
|
WHERE session_id=? AND revision=?`)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
.run(objective.trim(), cron, timezone, enabled ? 1 : 0, ownerUserId, (patch.notifyOnTerminal ?? current.notifyOnTerminal) ? 1 : 0, expectedRevision + 1, nextRunAt, now, sessionId, expectedRevision);
|
|
101
|
+
if (result.changes !== 1)
|
|
102
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
103
|
+
this.database
|
|
104
|
+
.prepare(`UPDATE scheduled_session_proposals SET status='STALE',resolved_at=?
|
|
90
105
|
WHERE task_id=? AND status='PENDING' AND base_revision<?`)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
.run(now, current.id, expectedRevision + 1);
|
|
107
|
+
this.database
|
|
108
|
+
.prepare("DELETE FROM scheduled_session_proposals WHERE task_id=? AND status='STALE'")
|
|
109
|
+
.run(current.id);
|
|
110
|
+
return this.get(sessionId);
|
|
111
|
+
});
|
|
96
112
|
}
|
|
97
113
|
remove(sessionId, expectedRevision) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
this.write(() => {
|
|
115
|
+
const task = this.get(sessionId);
|
|
116
|
+
if (task === undefined || task.revision !== expectedRevision)
|
|
117
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
118
|
+
this.deleteTasks('id', task.id);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
removeForSession(sessionId) {
|
|
122
|
+
this.write(() => this.deleteTasks('session_id', sessionId));
|
|
123
|
+
}
|
|
124
|
+
removeForWorkspace(workspaceId) {
|
|
125
|
+
this.write(() => this.deleteTasks('workspace_id', workspaceId));
|
|
126
|
+
}
|
|
127
|
+
deleteTasks(column, value) {
|
|
128
|
+
for (const table of ['scheduled_session_occurrences', 'scheduled_session_proposals']) {
|
|
129
|
+
this.database
|
|
130
|
+
.prepare(`DELETE FROM ${table} WHERE task_id IN
|
|
131
|
+
(SELECT id FROM scheduled_session_tasks WHERE ${column}=?)`)
|
|
132
|
+
.run(value);
|
|
133
|
+
}
|
|
134
|
+
this.database.prepare(`DELETE FROM scheduled_session_tasks WHERE ${column}=?`).run(value);
|
|
135
|
+
}
|
|
136
|
+
write(action) {
|
|
137
|
+
// A savepoint keeps this operation atomic both alone and inside a parent deletion.
|
|
138
|
+
this.database.exec('SAVEPOINT scheduled_session_write');
|
|
139
|
+
try {
|
|
140
|
+
const result = action();
|
|
141
|
+
this.database.exec('RELEASE scheduled_session_write');
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.database.exec('ROLLBACK TO scheduled_session_write; RELEASE scheduled_session_write');
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
103
148
|
}
|
|
104
149
|
claim(sessionId, dueAt, expectedRevision) {
|
|
105
150
|
const current = this.get(sessionId);
|
|
@@ -176,18 +221,25 @@ export class ScheduledSessionStore {
|
|
|
176
221
|
if (patch.objective === undefined && patch.cron === undefined && patch.timezone === undefined)
|
|
177
222
|
throw new Error('SCHEDULE_PROPOSAL_INVALID');
|
|
178
223
|
validate(patch.objective ?? task.objective, patch.cron ?? task.cron, patch.timezone ?? task.timezone);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
224
|
+
return this.write(() => {
|
|
225
|
+
const current = this.getById(task.id);
|
|
226
|
+
if (current === undefined)
|
|
227
|
+
throw new Error('SCHEDULE_NOT_FOUND');
|
|
228
|
+
if (current.revision !== task.revision || current.ownerUserId !== task.ownerUserId)
|
|
229
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
230
|
+
const pending = this.database
|
|
231
|
+
.prepare("SELECT COUNT(*) AS count FROM scheduled_session_proposals WHERE task_id=? AND status='PENDING'")
|
|
232
|
+
.get(task.id);
|
|
233
|
+
if (pending.count >= 50)
|
|
234
|
+
throw new Error('SCHEDULE_PROPOSAL_LIMIT');
|
|
235
|
+
const id = randomUUID();
|
|
236
|
+
this.database
|
|
237
|
+
.prepare(`INSERT INTO scheduled_session_proposals
|
|
187
238
|
(id,task_id,proposed_by_user_id,base_revision,objective,cron,timezone,status,created_at)
|
|
188
239
|
VALUES (?,?,?,?,?,?,?,'PENDING',?)`)
|
|
189
|
-
|
|
190
|
-
|
|
240
|
+
.run(id, task.id, actorUserId, task.revision, patch.objective ?? null, patch.cron ?? null, patch.timezone ?? null, this.now());
|
|
241
|
+
return id;
|
|
242
|
+
});
|
|
191
243
|
}
|
|
192
244
|
resolveProposal(sessionId, proposalId, ownerUserId, accept) {
|
|
193
245
|
const task = this.get(sessionId);
|
package/dist/terminal.js
CHANGED
|
@@ -186,6 +186,16 @@ export class TerminalManager {
|
|
|
186
186
|
get(terminalId) {
|
|
187
187
|
return this.summary(this.requireTerminal(terminalId));
|
|
188
188
|
}
|
|
189
|
+
/** A removed registration no longer owns live or historical Terminal metadata. */
|
|
190
|
+
removeWorkspaceAssociation(workspaceId) {
|
|
191
|
+
for (const terminal of this.terminals.values()) {
|
|
192
|
+
if (terminal.workspaceId !== workspaceId)
|
|
193
|
+
continue;
|
|
194
|
+
terminal.workspaceId = null;
|
|
195
|
+
terminal.updatedAt = this.now();
|
|
196
|
+
this.notifySummary(terminal);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
189
199
|
/** One attachment may write; active writers cannot be replaced. */
|
|
190
200
|
authorizeAttach(input) {
|
|
191
201
|
if (typeof input.terminalId !== 'string')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.173",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"playwright": "1.62.1",
|
|
37
37
|
"ws": "^8.21.3",
|
|
38
38
|
"zod": "4.4.3",
|
|
39
|
-
"@myagentroam/agent": "0.9.
|
|
39
|
+
"@myagentroam/agent": "0.9.173",
|
|
40
40
|
"@myagentroam/code-navigation": "0.9.129",
|
|
41
41
|
"@myagentroam/marctl": "0.9.123",
|
|
42
|
-
"@myagentroam/protocol": "0.9.
|
|
42
|
+
"@myagentroam/protocol": "0.9.173"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/ws": "^8.18.1"
|