@myagentroam/node 0.11.0 → 0.11.2
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 +20 -25
- package/dist/database.js +53 -31
- package/dist/migrations/v021.js +64 -0
- package/dist/service/node-run-event-bridge.js +0 -113
- package/dist/service/node-workspace-coordinator.js +21 -3
- package/dist/service/scheduled-session-coordinator.js +10 -12
- package/dist/service/skill-directory-service.js +2 -2
- package/dist/service/skill-install-service.js +15 -7
- package/dist/service/skill-node-operation-service.js +5 -4
- package/dist/service/workbench-event-projection.js +112 -0
- package/dist/service/workbench-event-service.js +24 -7
- package/dist/service/workspace-change-service.js +29 -13
- package/dist/service/workspace-file-service.js +3 -1
- package/dist/service/workspace-git-exclude-service.js +59 -32
- package/dist/service/workspace-service.js +34 -9
- package/dist/service/workspace-watch-snapshot-service.js +35 -5
- package/dist/service/workspace-workbench-service.js +23 -11
- package/dist/terminal.js +13 -12
- package/dist/workspace.js +27 -3
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -87,7 +87,8 @@ import { NodeControlChannel } from './service/node-control-channel.js';
|
|
|
87
87
|
import { NodeControlMessageService } from './service/node-control-message-service.js';
|
|
88
88
|
import { NodeRequestService } from './service/node-request-service.js';
|
|
89
89
|
import { WorkbenchManifestService } from './service/workbench-manifest-service.js';
|
|
90
|
-
import { NodeRunEventBridge
|
|
90
|
+
import { NodeRunEventBridge } from './service/node-run-event-bridge.js';
|
|
91
|
+
import { workbenchPayloadSessionId } from './service/workbench-event-projection.js';
|
|
91
92
|
import { NodeWorkspaceCoordinator } from './service/node-workspace-coordinator.js';
|
|
92
93
|
import { FakeManagedRunService } from './service/fake-managed-run-service.js';
|
|
93
94
|
import { RunnerChannelMessageService } from './service/runner-channel-message-service.js';
|
|
@@ -203,9 +204,18 @@ export class NodeConnector {
|
|
|
203
204
|
configPath;
|
|
204
205
|
reconnect;
|
|
205
206
|
claudeApprovalTimeoutMs;
|
|
206
|
-
workbenchEventService = new WorkbenchEventService(
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
workbenchEventService = new WorkbenchEventService({
|
|
208
|
+
send: (envelope) => this.sendEnvelope(envelope),
|
|
209
|
+
accepts: (category, payload) => {
|
|
210
|
+
const sessionId = workbenchPayloadSessionId((id) => this.runtime.getRun(id), category, payload);
|
|
211
|
+
return sessionId === undefined || !this.sessionIdentityService.bootstrapIds.has(sessionId);
|
|
212
|
+
},
|
|
213
|
+
projection: {
|
|
214
|
+
getRun: (id) => this.runtime.getRun(id),
|
|
215
|
+
conversationTurnForRun: (id) => this.runtime.conversationTurnForRun(id),
|
|
216
|
+
progress: (sessionId) => marctlSessionProgress(this.runtime, sessionId)
|
|
217
|
+
},
|
|
218
|
+
emitRealtime: (sessionId, event) => this.nativeSessionWatchService.emitRealtime(sessionId, event)
|
|
209
219
|
});
|
|
210
220
|
fakeRunner;
|
|
211
221
|
fakeScript;
|
|
@@ -1146,8 +1156,10 @@ export class NodeConnector {
|
|
|
1146
1156
|
nodeLog('node.connection.replaced', {
|
|
1147
1157
|
message: 'This Node connection was replaced by a newer instance with the same identity. Check for duplicate processes.'
|
|
1148
1158
|
});
|
|
1149
|
-
|
|
1150
|
-
|
|
1159
|
+
// Credential revocation ends terminals, but the control channel can still reconnect.
|
|
1160
|
+
// Only final Node disposal shuts down the manager and its idle collector.
|
|
1161
|
+
if (code === 4003)
|
|
1162
|
+
void this.terminalManager.closeAll();
|
|
1151
1163
|
}
|
|
1152
1164
|
});
|
|
1153
1165
|
this.controlMessages = new NodeControlMessageService({
|
|
@@ -1331,7 +1343,7 @@ export class NodeConnector {
|
|
|
1331
1343
|
this.workspaceCoordinator.clearWatches(workspaceId);
|
|
1332
1344
|
this.workspaceWorkbenchService.clearWorkspace(workspaceId);
|
|
1333
1345
|
this.promptCompletion?.invalidateWorkspace(workspaceId);
|
|
1334
|
-
});
|
|
1346
|
+
}, (workspaceId) => this.workspaceCoordinator.gitPolicyChanged(workspaceId));
|
|
1335
1347
|
this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
|
|
1336
1348
|
requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
|
|
1337
1349
|
inspectWorkspace: (workspace) => this.workspaceCoordinator.inspectMetadata(workspace),
|
|
@@ -1718,24 +1730,7 @@ export class NodeConnector {
|
|
|
1718
1730
|
}
|
|
1719
1731
|
this.emitWorkbenchEvent('session', payload);
|
|
1720
1732
|
};
|
|
1721
|
-
emitWorkbenchEvent = (category, payload) =>
|
|
1722
|
-
payload = this.runEventBridge.projectWorkbenchPayload(category, payload);
|
|
1723
|
-
if (payload === undefined)
|
|
1724
|
-
return;
|
|
1725
|
-
const event = this.workbenchEventService.next(category, payload);
|
|
1726
|
-
if (event === undefined)
|
|
1727
|
-
return;
|
|
1728
|
-
const notification = category === 'conversation' &&
|
|
1729
|
-
isPlainRecord(payload) &&
|
|
1730
|
-
isPlainRecord(payload.event) &&
|
|
1731
|
-
payload.event.eventType === 'notification';
|
|
1732
|
-
const sessionId = workbenchPayloadSessionId(this.runtime, category, payload);
|
|
1733
|
-
if (!notification &&
|
|
1734
|
-
sessionId !== undefined &&
|
|
1735
|
-
this.nativeSessionWatchService.emitRealtime(sessionId, event))
|
|
1736
|
-
return;
|
|
1737
|
-
this.workbenchEventService.publish(event);
|
|
1738
|
-
};
|
|
1733
|
+
emitWorkbenchEvent = (category, payload) => this.workbenchEventService.dispatch(category, payload);
|
|
1739
1734
|
discoverWorkspaceSessions = (workspaceId, onRunnerDiscovered) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId, onRunnerDiscovered);
|
|
1740
1735
|
invalidateWorkspaceWatchTopics = (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId);
|
|
1741
1736
|
reconcilePersistedRuns = () => undefined;
|
package/dist/database.js
CHANGED
|
@@ -2,6 +2,7 @@ import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
|
|
|
2
2
|
import { NODE_SCHEMA_MIGRATION_V18 } from './migrations/v018.js';
|
|
3
3
|
import { NODE_SCHEMA_MIGRATION_V19 } from './migrations/v019.js';
|
|
4
4
|
import { NODE_SCHEMA_MIGRATION_V20 } from './migrations/v020.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V21 } from './migrations/v021.js';
|
|
5
6
|
import { normalizeSessionPrompt } from './util/personal-instructions.js';
|
|
6
7
|
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
@@ -23,7 +24,8 @@ export const NODE_SCHEMA_MIGRATIONS = [
|
|
|
23
24
|
NODE_SCHEMA_MIGRATION_V17,
|
|
24
25
|
NODE_SCHEMA_MIGRATION_V18,
|
|
25
26
|
NODE_SCHEMA_MIGRATION_V19,
|
|
26
|
-
NODE_SCHEMA_MIGRATION_V20
|
|
27
|
+
NODE_SCHEMA_MIGRATION_V20,
|
|
28
|
+
NODE_SCHEMA_MIGRATION_V21
|
|
27
29
|
];
|
|
28
30
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
29
31
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -71,27 +73,22 @@ export class NodeDatabase {
|
|
|
71
73
|
this.raw.close();
|
|
72
74
|
}
|
|
73
75
|
getRunnerDefaults() {
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
const parsed = runnerDefaultConfigurationSchema.safeParse({
|
|
79
|
-
runner: stringValue(row, 'runner'),
|
|
80
|
-
model: stringValue(row, 'model'),
|
|
81
|
-
effort: stringValue(row, 'effort'),
|
|
82
|
-
access: stringValue(row, 'access')
|
|
83
|
-
});
|
|
76
|
+
return Object.entries(this.getNodeSettings().runnerDefaults ?? {})
|
|
77
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
78
|
+
.flatMap(([runner, settings]) => {
|
|
79
|
+
const parsed = runnerDefaultConfigurationSchema.safeParse({ runner, ...settings });
|
|
84
80
|
return parsed.success ? [parsed.data] : [];
|
|
85
81
|
});
|
|
86
82
|
}
|
|
83
|
+
getNodeSettings() {
|
|
84
|
+
const row = this.raw.prepare('SELECT settings FROM node_settings WHERE id = 1').get();
|
|
85
|
+
return JSON.parse(stringValue(row, 'settings'));
|
|
86
|
+
}
|
|
87
87
|
saveRunnerDefaults(input) {
|
|
88
|
+
const { runner, ...settings } = runnerDefaultConfigurationSchema.parse(input);
|
|
88
89
|
this.raw
|
|
89
|
-
.prepare(`
|
|
90
|
-
|
|
91
|
-
ON CONFLICT(runner) DO UPDATE SET
|
|
92
|
-
model = excluded.model, effort = excluded.effort,
|
|
93
|
-
access = excluded.access`)
|
|
94
|
-
.run(input.runner, input.model, input.effort, input.access);
|
|
90
|
+
.prepare(`UPDATE node_settings SET settings = json_patch(settings, ?) WHERE id = 1`)
|
|
91
|
+
.run(JSON.stringify({ runnerDefaults: { [runner]: settings } }));
|
|
95
92
|
return input;
|
|
96
93
|
}
|
|
97
94
|
listMcpInstallations(targetKind, targetId = '') {
|
|
@@ -188,45 +185,64 @@ export class NodeDatabase {
|
|
|
188
185
|
}
|
|
189
186
|
saveWorkspace(input) {
|
|
190
187
|
const current = this.raw
|
|
191
|
-
.prepare('SELECT id,
|
|
188
|
+
.prepare('SELECT id, settings FROM workspaces WHERE path = ?')
|
|
192
189
|
.get(input.path);
|
|
190
|
+
const settings = current === undefined
|
|
191
|
+
? {
|
|
192
|
+
git: {
|
|
193
|
+
preserveGitConfig: input.preserveGitConfig ?? false,
|
|
194
|
+
includeParentRepository: input.includeParentRepository ?? true
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
: JSON.parse(stringValue(current, 'settings'));
|
|
193
198
|
const workspace = {
|
|
194
199
|
id: current === undefined ? (input.id ?? randomUUID()) : stringValue(current, 'id'),
|
|
195
200
|
path: input.path,
|
|
196
201
|
kind: input.kind,
|
|
197
202
|
gitSummary: input.gitSummary,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
settings,
|
|
204
|
+
includeParentRepository: settings.git?.includeParentRepository ?? true,
|
|
205
|
+
preserveGitConfig: settings.git?.preserveGitConfig ?? false
|
|
201
206
|
};
|
|
202
207
|
const timestamp = this.now();
|
|
203
208
|
this.raw
|
|
204
|
-
.prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at,
|
|
209
|
+
.prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at, settings)
|
|
205
210
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
206
211
|
ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,
|
|
207
212
|
git_summary = excluded.git_summary, updated_at = excluded.updated_at`)
|
|
208
|
-
.run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp,
|
|
213
|
+
.run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp, JSON.stringify(settings));
|
|
209
214
|
return workspace;
|
|
210
215
|
}
|
|
211
216
|
listWorkspaces() {
|
|
212
217
|
return this.raw
|
|
213
|
-
.prepare('SELECT id, path, kind, git_summary,
|
|
218
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces ORDER BY created_at ASC')
|
|
214
219
|
.all()
|
|
215
220
|
.map((row) => workspace(row));
|
|
216
221
|
}
|
|
217
222
|
getWorkspace(id) {
|
|
218
223
|
const row = this.raw
|
|
219
|
-
.prepare('SELECT id, path, kind, git_summary,
|
|
224
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE id = ?')
|
|
220
225
|
.get(id);
|
|
221
226
|
return row === undefined ? undefined : workspace(row);
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
getWorkspaceByPath(path) {
|
|
229
|
+
const row = this.raw
|
|
230
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE path = ?')
|
|
231
|
+
.get(path);
|
|
232
|
+
return row === undefined ? undefined : workspace(row);
|
|
233
|
+
}
|
|
234
|
+
/** Updates only supplied policy fields without replacing Workspace metadata. */
|
|
235
|
+
setWorkspaceGitPolicy(id, preserveGitConfig, includeParentRepository) {
|
|
225
236
|
if (this.getWorkspace(id) === undefined)
|
|
226
237
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
227
238
|
this.raw
|
|
228
|
-
.prepare('UPDATE workspaces SET
|
|
229
|
-
.run(
|
|
239
|
+
.prepare('UPDATE workspaces SET settings=json_patch(settings, ?), updated_at=? WHERE id=?')
|
|
240
|
+
.run(JSON.stringify({
|
|
241
|
+
git: {
|
|
242
|
+
preserveGitConfig,
|
|
243
|
+
...(includeParentRepository === undefined ? {} : { includeParentRepository })
|
|
244
|
+
}
|
|
245
|
+
}), this.now(), id);
|
|
230
246
|
return this.getWorkspace(id);
|
|
231
247
|
}
|
|
232
248
|
/** Unregisters a Workspace registration without deleting its local directory. */
|
|
@@ -474,7 +490,10 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
474
490
|
throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
|
|
475
491
|
if (migratedVersions.some((version, index) => version !== NODE_SCHEMA_VERSION_V13 + index))
|
|
476
492
|
throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
|
|
477
|
-
|
|
493
|
+
if (current < NODE_SCHEMA_MIGRATION_V21.version)
|
|
494
|
+
assertNodeSchemaV13(database);
|
|
495
|
+
else
|
|
496
|
+
catalog[current - NODE_SCHEMA_VERSION_V13 - 1].assert(database);
|
|
478
497
|
for (const entry of catalog) {
|
|
479
498
|
if (entry.version <= current)
|
|
480
499
|
continue;
|
|
@@ -623,9 +642,12 @@ function acquireNodeMigrationLock(databasePath) {
|
|
|
623
642
|
}
|
|
624
643
|
function workspace(row) {
|
|
625
644
|
const summary = nullableString(row, 'git_summary');
|
|
645
|
+
const settings = JSON.parse(stringValue(row, 'settings'));
|
|
626
646
|
return {
|
|
627
647
|
id: stringValue(row, 'id'),
|
|
628
|
-
|
|
648
|
+
settings,
|
|
649
|
+
preserveGitConfig: settings.git?.preserveGitConfig ?? false,
|
|
650
|
+
includeParentRepository: settings.git?.includeParentRepository ?? true,
|
|
629
651
|
path: stringValue(row, 'path'),
|
|
630
652
|
kind: stringValue(row, 'kind'),
|
|
631
653
|
gitSummary: summary === null ? null : JSON.parse(summary)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
|
|
6
|
+
import { NODE_SCHEMA_MIGRATION_V17 } from './v017.js';
|
|
7
|
+
import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
|
|
8
|
+
import { NODE_SCHEMA_MIGRATION_V19 } from './v019.js';
|
|
9
|
+
import { NODE_SCHEMA_MIGRATION_V20 } from './v020.js';
|
|
10
|
+
const sql = `ALTER TABLE workspaces ADD COLUMN settings TEXT NOT NULL DEFAULT '{}'
|
|
11
|
+
CHECK (json_valid(settings) AND json_type(settings) = 'object');
|
|
12
|
+
UPDATE workspaces SET settings = json_object('git', json_object(
|
|
13
|
+
'preserveGitConfig', json(CASE WHEN preserve_git_config = 1 THEN 'true' ELSE 'false' END),
|
|
14
|
+
'includeParentRepository', json('true')));
|
|
15
|
+
ALTER TABLE workspaces DROP COLUMN preserve_git_config;
|
|
16
|
+
CREATE TABLE node_settings (
|
|
17
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
18
|
+
settings TEXT NOT NULL CHECK (json_valid(settings) AND json_type(settings) = 'object')
|
|
19
|
+
) STRICT;
|
|
20
|
+
INSERT INTO node_settings (id, settings)
|
|
21
|
+
SELECT 1, json_object('runnerDefaults', json_group_object(runner,
|
|
22
|
+
json_object('model', model, 'effort', effort, 'access', access))) FROM runner_defaults;
|
|
23
|
+
DROP TABLE runner_defaults;`;
|
|
24
|
+
export const NODE_SCHEMA_MIGRATION_V21 = {
|
|
25
|
+
version: 21,
|
|
26
|
+
sql,
|
|
27
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V20.assert(database),
|
|
28
|
+
assert(database) {
|
|
29
|
+
const reference = new DatabaseSync(':memory:');
|
|
30
|
+
try {
|
|
31
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
32
|
+
for (const migration of [
|
|
33
|
+
NODE_SCHEMA_MIGRATION_V14,
|
|
34
|
+
NODE_SCHEMA_MIGRATION_V15,
|
|
35
|
+
NODE_SCHEMA_MIGRATION_V16,
|
|
36
|
+
NODE_SCHEMA_MIGRATION_V17,
|
|
37
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
38
|
+
NODE_SCHEMA_MIGRATION_V19,
|
|
39
|
+
NODE_SCHEMA_MIGRATION_V20
|
|
40
|
+
])
|
|
41
|
+
reference.exec(migration.sql);
|
|
42
|
+
reference.exec(sql);
|
|
43
|
+
const schema = (db) => db
|
|
44
|
+
.prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
|
|
45
|
+
.all()
|
|
46
|
+
.map((row) => [
|
|
47
|
+
row.type,
|
|
48
|
+
row.name,
|
|
49
|
+
row.name === 'node_schema_migrations'
|
|
50
|
+
? String(row.sql).replace(/\s+/g, '')
|
|
51
|
+
: String(row.sql).replace(/\s+/g, ' ').trim()
|
|
52
|
+
]);
|
|
53
|
+
const expected = schema(reference);
|
|
54
|
+
const actual = schema(database);
|
|
55
|
+
const mismatch = expected.find((row) => !actual.some((value) => JSON.stringify(value) === JSON.stringify(row))) ??
|
|
56
|
+
actual.find((row) => !expected.some((value) => JSON.stringify(value) === JSON.stringify(row)));
|
|
57
|
+
if (mismatch !== undefined)
|
|
58
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${String(mismatch[1])}`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
reference.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -1,122 +1,9 @@
|
|
|
1
|
-
import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
|
|
2
|
-
import { runConversationItemId } from '../runtime-state.js';
|
|
3
|
-
import { marctlSessionProgress } from './session-recent-activity.js';
|
|
4
|
-
import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
5
|
-
export function workbenchPayloadSessionId(runtime, category, payload) {
|
|
6
|
-
if (!isPlainRecord(payload))
|
|
7
|
-
return undefined;
|
|
8
|
-
if (category === 'session') {
|
|
9
|
-
if (isPlainRecord(payload.session)) {
|
|
10
|
-
const id = payload.session.id;
|
|
11
|
-
if (typeof id === 'string')
|
|
12
|
-
return id;
|
|
13
|
-
}
|
|
14
|
-
return typeof payload.sessionId === 'string' ? payload.sessionId : undefined;
|
|
15
|
-
}
|
|
16
|
-
if (category === 'run' && isNodeAgentRun(payload.run))
|
|
17
|
-
return payload.run.sessionId;
|
|
18
|
-
if (category === 'conversation') {
|
|
19
|
-
if (isNodeConversationTurn(payload.turn))
|
|
20
|
-
return payload.turn.sessionId;
|
|
21
|
-
if (typeof payload.sessionId === 'string')
|
|
22
|
-
return payload.sessionId;
|
|
23
|
-
if (isPlainRecord(payload.event)) {
|
|
24
|
-
const runId = payload.event.runId;
|
|
25
|
-
return typeof runId === 'string' ? runtime.getRun(runId)?.sessionId : undefined;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (category === 'queue') {
|
|
29
|
-
const id = payload.sessionId;
|
|
30
|
-
return typeof id === 'string' ? id : undefined;
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
1
|
/** Bridges process-local Run state to normalized control and Workbench events. */
|
|
35
2
|
export class NodeRunEventBridge {
|
|
36
3
|
options;
|
|
37
4
|
constructor(options) {
|
|
38
5
|
this.options = options;
|
|
39
6
|
}
|
|
40
|
-
/** The sole public projection boundary for normalized Run callbacks. */
|
|
41
|
-
projectWorkbenchPayload(category, payload) {
|
|
42
|
-
if (category === 'conversation' &&
|
|
43
|
-
isPlainRecord(payload) &&
|
|
44
|
-
isPlainRecord(payload.event) &&
|
|
45
|
-
payload.event.eventType !== 'notification') {
|
|
46
|
-
const runId = typeof payload.event.runId === 'string' ? payload.event.runId : undefined;
|
|
47
|
-
const sequence = typeof payload.event.sequence === 'number' ? payload.event.sequence : undefined;
|
|
48
|
-
const turn = runId === undefined ? undefined : this.options.runtime.conversationTurnForRun(runId);
|
|
49
|
-
if (turn === undefined)
|
|
50
|
-
return;
|
|
51
|
-
const rawPayload = isPlainRecord(payload.event.payload) ? payload.event.payload : undefined;
|
|
52
|
-
const text = payload.event.eventType === 'text.delta' && typeof rawPayload?.text === 'string'
|
|
53
|
-
? rawPayload.text
|
|
54
|
-
: undefined;
|
|
55
|
-
const nativeItemId = typeof rawPayload?.itemId === 'string'
|
|
56
|
-
? rawPayload.itemId
|
|
57
|
-
: typeof rawPayload?.approvalId === 'string'
|
|
58
|
-
? `approval:${rawPayload.approvalId}`
|
|
59
|
-
: undefined;
|
|
60
|
-
const item = text !== undefined && sequence !== undefined
|
|
61
|
-
? turn.items.find((candidate) => candidate.kind === 'assistant_message' && candidate.revision === sequence)
|
|
62
|
-
: nativeItemId === undefined
|
|
63
|
-
? undefined
|
|
64
|
-
: turn.items.find((candidate) => candidate.id ===
|
|
65
|
-
runConversationItemId(runId, nativeItemId, typeof rawPayload?.kind === 'string' ? rawPayload.kind : undefined));
|
|
66
|
-
if (text !== undefined && item?.kind === 'assistant_message') {
|
|
67
|
-
const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
|
|
68
|
-
payload = {
|
|
69
|
-
sessionId: turn.sessionId,
|
|
70
|
-
delta: {
|
|
71
|
-
turnId: turn.id,
|
|
72
|
-
segmentId: segment.id,
|
|
73
|
-
segmentIndex: segment.index,
|
|
74
|
-
runId: turn.runId,
|
|
75
|
-
itemId: item.id,
|
|
76
|
-
text,
|
|
77
|
-
createdAt: typeof payload.event.createdAt === 'number' ? payload.event.createdAt : Date.now()
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
else if (item !== undefined) {
|
|
82
|
-
const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
|
|
83
|
-
payload = {
|
|
84
|
-
sessionId: turn.sessionId,
|
|
85
|
-
runId: turn.runId,
|
|
86
|
-
...(['assistant_message', 'tool_call', 'command_execution', 'file_change'].includes(item.kind) &&
|
|
87
|
-
(rawPayload?.append === undefined || item.status !== 'IN_PROGRESS')
|
|
88
|
-
? { progress: marctlSessionProgress(this.options.runtime, turn.sessionId) }
|
|
89
|
-
: {}),
|
|
90
|
-
item: {
|
|
91
|
-
turnId: turn.id,
|
|
92
|
-
itemId: item.id,
|
|
93
|
-
kind: item.kind,
|
|
94
|
-
status: item.status,
|
|
95
|
-
payload: item.payload,
|
|
96
|
-
startedAt: item.startedAt,
|
|
97
|
-
completedAt: item.completedAt,
|
|
98
|
-
segmentId: segment.id,
|
|
99
|
-
segmentIndex: segment.index
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
if (payload.event.eventType !== 'run.started' && payload.event.eventType !== 'run.running')
|
|
105
|
-
return;
|
|
106
|
-
payload = { turn };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (category === 'conversation' &&
|
|
110
|
-
isPlainRecord(payload) &&
|
|
111
|
-
isNodeConversationTurn(payload.turn))
|
|
112
|
-
return { ...payload, segments: conversationSegments(payload.turn) };
|
|
113
|
-
if (category === 'run' && isPlainRecord(payload) && isNodeAgentRun(payload.run))
|
|
114
|
-
return {
|
|
115
|
-
...payload,
|
|
116
|
-
progress: marctlSessionProgress(this.options.runtime, payload.run.sessionId)
|
|
117
|
-
};
|
|
118
|
-
return payload;
|
|
119
|
-
}
|
|
120
7
|
emitConversationItem(runId, item) {
|
|
121
8
|
this.emitRun(runId, 'conversation.item', item);
|
|
122
9
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { nodeLog } from '../operational.js';
|
|
1
3
|
import { inspectWorkspace, normalizeWorkspacePath } from '../workspace.js';
|
|
2
4
|
import { ensureWorkspaceGitExclude } from './workspace-git-exclude-service.js';
|
|
3
5
|
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
@@ -18,7 +20,7 @@ export class NodeWorkspaceCoordinator {
|
|
|
18
20
|
const config = this.options.config();
|
|
19
21
|
if (config === undefined)
|
|
20
22
|
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
21
|
-
const inspection = await inspectWorkspace(workspace.path, config.allowedRoots);
|
|
23
|
+
const inspection = await inspectWorkspace(workspace.path, config.allowedRoots, workspace.includeParentRepository ?? true);
|
|
22
24
|
if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
|
|
23
25
|
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
24
26
|
return inspection;
|
|
@@ -29,8 +31,14 @@ export class NodeWorkspaceCoordinator {
|
|
|
29
31
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
30
32
|
if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
|
|
31
33
|
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
34
|
+
const current = this.require(workspace.id);
|
|
35
|
+
if ((current.includeParentRepository ?? true) !== (workspace.includeParentRepository ?? true))
|
|
36
|
+
return current;
|
|
32
37
|
if (workspace.kind !== 'GIT_WORKSPACE' && inspection.kind === 'GIT_WORKSPACE')
|
|
33
|
-
await ensureWorkspaceGitExclude(workspace.path);
|
|
38
|
+
await ensureWorkspaceGitExclude(workspace.path, workspace.includeParentRepository ?? true);
|
|
39
|
+
if ((this.require(workspace.id).includeParentRepository ?? true) !==
|
|
40
|
+
(workspace.includeParentRepository ?? true))
|
|
41
|
+
return this.require(workspace.id);
|
|
34
42
|
const saved = database.saveWorkspace({
|
|
35
43
|
id: workspace.id,
|
|
36
44
|
path: workspace.path,
|
|
@@ -57,6 +65,14 @@ export class NodeWorkspaceCoordinator {
|
|
|
57
65
|
invalidateChanges(workspaceId) {
|
|
58
66
|
this.options.changes.invalidate(workspaceId);
|
|
59
67
|
}
|
|
68
|
+
gitPolicyChanged(workspaceId) {
|
|
69
|
+
this.invalidate(workspaceId);
|
|
70
|
+
const workspace = this.require(workspaceId);
|
|
71
|
+
void this.inspectMetadata(workspace)
|
|
72
|
+
.then((inspection) => this.commitMetadata(workspace, inspection))
|
|
73
|
+
.then(() => this.invalidate(workspaceId))
|
|
74
|
+
.catch(() => nodeLog('workspace.git-policy.refresh-failed', { workspaceId }));
|
|
75
|
+
}
|
|
60
76
|
persistTerminal(summary) {
|
|
61
77
|
const database = this.options.database();
|
|
62
78
|
if (database === undefined)
|
|
@@ -87,7 +103,9 @@ export class NodeWorkspaceCoordinator {
|
|
|
87
103
|
return;
|
|
88
104
|
}
|
|
89
105
|
try {
|
|
90
|
-
const
|
|
106
|
+
const path = await realpath(requestedPath);
|
|
107
|
+
const current = this.options.database()?.getWorkspaceByPath(path);
|
|
108
|
+
const inspection = await inspectWorkspace(path, config.allowedRoots, current?.includeParentRepository ?? true);
|
|
91
109
|
this.options.send('workspace.inspect.result', inspection, envelope.id);
|
|
92
110
|
}
|
|
93
111
|
catch {
|
|
@@ -16,24 +16,22 @@ export class ScheduledSessionCoordinator {
|
|
|
16
16
|
const store = this.options.store();
|
|
17
17
|
store.finish(run.clientMessageId, status === 'SUCCEEDED' ? 'SUCCEEDED' : 'FAILED', status === 'SUCCEEDED' ? undefined : `SCHEDULE_RUN_${status}`, run.id);
|
|
18
18
|
const task = store.getById(run.clientMessageId.split(':', 1)[0]);
|
|
19
|
-
if (task !== undefined)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
else if (status !== 'SUCCEEDED')
|
|
27
|
-
this.notify(task, run.clientMessageId, `SCHEDULE_RUN_${status}`, 'FAILED');
|
|
28
|
-
}
|
|
19
|
+
if (task !== undefined)
|
|
20
|
+
this.publishTaskNotification(task, run.clientMessageId, {
|
|
21
|
+
kind: 'SCHEDULE_TERMINAL',
|
|
22
|
+
outcome: status,
|
|
23
|
+
runId: run.id
|
|
24
|
+
});
|
|
29
25
|
}
|
|
30
26
|
notify = (task, occurrenceId, code, status) => this.publishTaskNotification(task, occurrenceId, {
|
|
31
27
|
kind: status === 'FAILED' ? 'SCHEDULE_FAILED' : 'SCHEDULE_SKIPPED',
|
|
32
28
|
code
|
|
33
29
|
});
|
|
34
30
|
publishTaskNotification = (task, occurrenceId, event) => {
|
|
31
|
+
const currentTask = this.options.store().getById(task.id) ?? task;
|
|
32
|
+
if (!currentTask.notifyOnTerminal)
|
|
33
|
+
return;
|
|
35
34
|
const occurredAt = Date.now();
|
|
36
|
-
const recipient = this.options.store().getById(task.id)?.ownerUserId ?? task.ownerUserId;
|
|
37
35
|
this.options.emitNotification({
|
|
38
36
|
event: {
|
|
39
37
|
runId: event.kind === 'SCHEDULE_TERMINAL' ? event.runId : occurrenceId,
|
|
@@ -41,7 +39,7 @@ export class ScheduledSessionCoordinator {
|
|
|
41
39
|
eventType: 'notification',
|
|
42
40
|
payload: {
|
|
43
41
|
targetType: 'WORKSPACE_SESSION',
|
|
44
|
-
userId:
|
|
42
|
+
userId: currentTask.ownerUserId,
|
|
45
43
|
workspaceId: task.workspaceId,
|
|
46
44
|
sessionId: task.sessionId,
|
|
47
45
|
sessionTitle: this.options.sessionTitle(task.sessionId),
|
|
@@ -22,11 +22,11 @@ export class SkillDirectoryService {
|
|
|
22
22
|
: agents.map(withoutSource)
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
async inspectWorkspace(workspace) {
|
|
25
|
+
async inspectWorkspace(workspace, includeParentRepository = true) {
|
|
26
26
|
const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
|
|
27
27
|
const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
|
|
28
28
|
const skills = mergeWorkspaceCopies(agents, claude);
|
|
29
|
-
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name));
|
|
29
|
+
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name), includeParentRepository);
|
|
30
30
|
return {
|
|
31
31
|
targetKind: 'WORKSPACE',
|
|
32
32
|
compatibilityMode: 'MANAGED_COPY',
|
|
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { skillInstallMetadataSchema } from '@myagentroam/protocol';
|
|
7
|
+
import { GitCommandError, resolveWorkspaceGitRoot } from '../workspace.js';
|
|
7
8
|
import { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
|
|
8
9
|
export class SkillInstallService {
|
|
9
10
|
async recoverNodeHome(home) {
|
|
@@ -59,15 +60,15 @@ export class SkillInstallService {
|
|
|
59
60
|
await mkdir(claudeRoot, { recursive: true });
|
|
60
61
|
await assertContainedDirectory(workspace, agentsRoot);
|
|
61
62
|
await assertContainedDirectory(workspace, claudeRoot);
|
|
62
|
-
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true), () => updateWorkspaceSkillGitExclude(workspace, name, false));
|
|
63
|
+
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true, options.includeParentRepository ?? true), () => updateWorkspaceSkillGitExclude(workspace, name, false, options.includeParentRepository ?? true));
|
|
63
64
|
}
|
|
64
|
-
async removeWorkspace(workspace, name) {
|
|
65
|
+
async removeWorkspace(workspace, name, includeParentRepository = true) {
|
|
65
66
|
assertName(name);
|
|
66
67
|
const agentsRoot = path.join(workspace, '.agents', 'skills');
|
|
67
68
|
const claudeRoot = path.join(workspace, '.claude', 'skills');
|
|
68
69
|
await assertExistingContainedDirectory(workspace, agentsRoot);
|
|
69
70
|
await assertExistingContainedDirectory(workspace, claudeRoot);
|
|
70
|
-
await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false), () => updateWorkspaceSkillGitExclude(workspace, name, true));
|
|
71
|
+
await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false, includeParentRepository), () => updateWorkspaceSkillGitExclude(workspace, name, true, includeParentRepository));
|
|
71
72
|
}
|
|
72
73
|
async removeNodeHome(home, name) {
|
|
73
74
|
assertName(name);
|
|
@@ -81,7 +82,7 @@ export class SkillInstallService {
|
|
|
81
82
|
else
|
|
82
83
|
await removeSingle(agentsRoot, name);
|
|
83
84
|
}
|
|
84
|
-
async repairWorkspaceGitExclude(workspace) {
|
|
85
|
+
async repairWorkspaceGitExclude(workspace, includeParentRepository = true) {
|
|
85
86
|
const root = path.join(workspace, '.agents', 'skills');
|
|
86
87
|
const names = new Set();
|
|
87
88
|
try {
|
|
@@ -101,13 +102,20 @@ export class SkillInstallService {
|
|
|
101
102
|
if (!isMissing(error))
|
|
102
103
|
throw error;
|
|
103
104
|
}
|
|
104
|
-
await rewriteWorkspaceGitExclude(workspace, names);
|
|
105
|
+
await rewriteWorkspaceGitExclude(workspace, names, includeParentRepository);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
export async function workspaceSkillTracked(workspace, name) {
|
|
108
109
|
assertName(name);
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
try {
|
|
111
|
+
await resolveWorkspaceGitRoot(workspace);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if ((error instanceof GitCommandError && error.code === 128) ||
|
|
115
|
+
error.code === 'ENOENT')
|
|
116
|
+
return false;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
111
119
|
try {
|
|
112
120
|
const result = await promisify(execFile)('git', ['-C', workspace, 'ls-files', '--', `.agents/skills/${name}`, `.claude/skills/${name}`], { timeout: 5_000, windowsHide: true });
|
|
113
121
|
return result.stdout.trim() !== '';
|
|
@@ -28,14 +28,14 @@ export class SkillNodeOperationService {
|
|
|
28
28
|
'workspace.skills.inspect': async (data) => {
|
|
29
29
|
const workspace = this.workspacePath(record(data)['workspaceId']);
|
|
30
30
|
await this.installer.recoverWorkspace(workspace);
|
|
31
|
-
return this.directories.inspectWorkspace(workspace);
|
|
31
|
+
return this.directories.inspectWorkspace(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
32
32
|
},
|
|
33
33
|
'workspace.skills.install': (data) => this.install('WORKSPACE', data),
|
|
34
34
|
'workspace.skills.remove': (data) => this.remove('WORKSPACE', data),
|
|
35
35
|
'workspace.skills.git-exclude-repair': async (data) => {
|
|
36
36
|
const workspace = this.workspacePath(record(data)['workspaceId']);
|
|
37
37
|
await this.installer.recoverWorkspace(workspace);
|
|
38
|
-
await this.installer.repairWorkspaceGitExclude(workspace);
|
|
38
|
+
await this.installer.repairWorkspaceGitExclude(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
39
39
|
return { repaired: true };
|
|
40
40
|
},
|
|
41
41
|
'skill.install.job.get': (data) => this.job(data)
|
|
@@ -113,7 +113,8 @@ export class SkillNodeOperationService {
|
|
|
113
113
|
await this.installer.installNodeHome(homedir(), input.name, bundle, input.metadata);
|
|
114
114
|
else
|
|
115
115
|
await this.installer.installWorkspace(input.workspace, input.name, bundle, input.metadata, {
|
|
116
|
-
confirmTracked: true
|
|
116
|
+
confirmTracked: true,
|
|
117
|
+
includeParentRepository: this.database().getWorkspaceByPath(input.workspace)?.includeParentRepository ?? true
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
async remove(target, data) {
|
|
@@ -131,7 +132,7 @@ export class SkillNodeOperationService {
|
|
|
131
132
|
}
|
|
132
133
|
else {
|
|
133
134
|
await this.installer.recoverWorkspace(workspace);
|
|
134
|
-
await this.installer.removeWorkspace(workspace, name);
|
|
135
|
+
await this.installer.removeWorkspace(workspace, name, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
finally {
|