@myagentroam/node 0.12.4 → 0.12.6
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/application/scheduled-execution-service.js +60 -0
- package/dist/connector.js +207 -261
- package/dist/marctl/package.json +1 -1
- package/dist/runner/claude-code-runner.js +4 -0
- package/dist/runner/codex-runner.js +11 -4
- package/dist/runner/mar-agent/agent-network-extension.js +2 -1
- package/dist/runner/opencode/opencode-server.js +37 -12
- package/dist/runner/opencode-runner.js +21 -7
- package/dist/service/agent-network-run-projection.js +60 -0
- package/dist/service/composer-message-input.js +1 -1
- package/dist/service/session-recent-activity.js +3 -3
- package/dist/service/skill-directory-service.js +10 -31
- package/dist/service/skill-node-operation-service.js +3 -5
- package/dist/session/native-session-projection-service.js +41 -6
- package/dist/session/session-catalog-service.js +11 -0
- package/dist/workspace/workspace-session-service.js +58 -8
- package/package.json +4 -4
|
@@ -389,16 +389,18 @@ export class CodexRunner extends AbstractRunner {
|
|
|
389
389
|
return { failureCode: 'RUNNER_RESUME_FAILED' };
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
|
-
async cleanupThreads() {
|
|
392
|
+
async cleanupThreads(signal) {
|
|
393
393
|
await this.start();
|
|
394
394
|
const threads = [];
|
|
395
395
|
const cursors = new Set();
|
|
396
396
|
let cursor;
|
|
397
397
|
for (;;) {
|
|
398
|
+
signal?.throwIfAborted();
|
|
398
399
|
const result = await this.client.listThreads({
|
|
399
400
|
limit: 200,
|
|
400
401
|
...(cursor === undefined ? {} : { cursor })
|
|
401
402
|
});
|
|
403
|
+
signal?.throwIfAborted();
|
|
402
404
|
if (!isPlainRecord(result) ||
|
|
403
405
|
!Array.isArray(result.data) ||
|
|
404
406
|
result.data.some((entry) => !isPlainRecord(entry) ||
|
|
@@ -418,21 +420,25 @@ export class CodexRunner extends AbstractRunner {
|
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
422
|
async discoverSessions(context, workspace) {
|
|
423
|
+
context.signal?.throwIfAborted();
|
|
421
424
|
const [threads, nativeSessions] = context.cleanup
|
|
422
|
-
? [await this.cleanupThreads(), []]
|
|
425
|
+
? [await this.cleanupThreads(context.signal), []]
|
|
423
426
|
: await Promise.all([
|
|
424
427
|
(async () => {
|
|
425
428
|
try {
|
|
426
429
|
await this.start();
|
|
427
|
-
|
|
430
|
+
context.signal?.throwIfAborted();
|
|
431
|
+
return extractCodexThreads(await this.listThreads(context.signal));
|
|
428
432
|
}
|
|
429
433
|
catch {
|
|
434
|
+
context.signal?.throwIfAborted();
|
|
430
435
|
// Native transcripts remain a valid read-only discovery source.
|
|
431
436
|
return [];
|
|
432
437
|
}
|
|
433
438
|
})(),
|
|
434
439
|
discoverAllNativeSessions(this.name)
|
|
435
440
|
]);
|
|
441
|
+
context.signal?.throwIfAborted();
|
|
436
442
|
const sessions = [];
|
|
437
443
|
for (const thread of threads) {
|
|
438
444
|
if (thread.derived || (thread.source !== 'cli' && thread.source !== 'vscode'))
|
|
@@ -642,8 +648,9 @@ export class CodexRunner extends AbstractRunner {
|
|
|
642
648
|
respond(requestId, result, runId) {
|
|
643
649
|
this.clientFor(runId).respond(requestId, result);
|
|
644
650
|
}
|
|
645
|
-
async listThreads() {
|
|
651
|
+
async listThreads(signal) {
|
|
646
652
|
const result = await this.client.listThreads();
|
|
653
|
+
signal?.throwIfAborted();
|
|
647
654
|
this.observeThreadHistoryModes(result);
|
|
648
655
|
return result;
|
|
649
656
|
}
|
|
@@ -129,7 +129,8 @@ const collaborationPrompt = [
|
|
|
129
129
|
"Use native agent_* tools to collaborate within the Agent Network when another Session's context or ongoing work materially helps. Prefer focused requests and avoid unnecessary cross-Session coordination. Peer Sessions retain their own work and lifecycle; do not treat them as task-owned children or stop them merely for task cleanup.",
|
|
130
130
|
'Prefer reusing known Session IDs rather than rediscovering them. Updates are most useful when requested or when the recipient needs to decide or change course; prefer combining related updates.',
|
|
131
131
|
'An accepted message normally needs no separate acknowledgment. Requests for receipt-only replies and immediate agent_view calls solely to confirm receipt are usually unnecessary.',
|
|
132
|
-
'
|
|
132
|
+
'The parent automatically collects the latest unread public reply and terminal state from each observed direct child before its next natural model request. Usually reply in the child Session itself rather than sending duplicate progress or completion reports with agent_send.',
|
|
133
|
+
'Contact the parent proactively when timely action or a decision is needed, such as a blocker, scope conflict or major risk, or when explicitly asked to report. Routine child updates usually need no reply; prefer combining related guidance.'
|
|
133
134
|
].join('\n\n');
|
|
134
135
|
const managementPrompt = [
|
|
135
136
|
'# Workspace management',
|
|
@@ -4,11 +4,15 @@ import { createServer } from 'node:net';
|
|
|
4
4
|
import { minimalRunnerEnvironment, platformCliInvocation } from '../../operational.js';
|
|
5
5
|
import { terminateProcessTree } from '../../process-tree.js';
|
|
6
6
|
const START_TIMEOUT_MS = 10_000;
|
|
7
|
+
/** Owns one supervised backend and its startup I/O. Request cancellation is caller-scoped;
|
|
8
|
+
* Stopping cancels startup and joins it along with child termination.
|
|
9
|
+
*/
|
|
7
10
|
export class OpenCodeServerClient {
|
|
8
11
|
options;
|
|
9
12
|
child;
|
|
10
13
|
client;
|
|
11
14
|
starting;
|
|
15
|
+
startingController;
|
|
12
16
|
environment = {};
|
|
13
17
|
constructor(options = {}) {
|
|
14
18
|
this.options = options;
|
|
@@ -23,19 +27,25 @@ export class OpenCodeServerClient {
|
|
|
23
27
|
return;
|
|
24
28
|
if (this.starting !== undefined)
|
|
25
29
|
return this.starting;
|
|
26
|
-
const
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const starting = this.startServer(controller.signal);
|
|
32
|
+
this.startingController = controller;
|
|
27
33
|
this.starting = starting;
|
|
28
34
|
try {
|
|
29
35
|
await starting;
|
|
30
36
|
}
|
|
31
37
|
finally {
|
|
32
|
-
if (this.starting === starting)
|
|
38
|
+
if (this.starting === starting) {
|
|
33
39
|
this.starting = undefined;
|
|
40
|
+
this.startingController = undefined;
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
|
-
async startServer() {
|
|
44
|
+
async startServer(signal) {
|
|
37
45
|
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2/client');
|
|
46
|
+
signal.throwIfAborted();
|
|
38
47
|
const port = await availablePort();
|
|
48
|
+
signal.throwIfAborted();
|
|
39
49
|
const password = randomBytes(24).toString('base64url');
|
|
40
50
|
const invocation = platformCliInvocation(this.options.command ?? 'opencode', [
|
|
41
51
|
'serve',
|
|
@@ -65,11 +75,12 @@ export class OpenCodeServerClient {
|
|
|
65
75
|
throwOnError: true
|
|
66
76
|
});
|
|
67
77
|
try {
|
|
68
|
-
await waitUntilHealthy(`http://127.0.0.1:${port}/global/health`, authorization, child, this.options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
78
|
+
await waitUntilHealthy(`http://127.0.0.1:${port}/global/health`, authorization, child, this.options.startTimeoutMs ?? START_TIMEOUT_MS, signal);
|
|
79
|
+
signal.throwIfAborted();
|
|
69
80
|
this.client = client;
|
|
70
81
|
}
|
|
71
82
|
catch (error) {
|
|
72
|
-
await this.
|
|
83
|
+
await this.stopChild(child);
|
|
73
84
|
throw error;
|
|
74
85
|
}
|
|
75
86
|
}
|
|
@@ -89,9 +100,11 @@ export class OpenCodeServerClient {
|
|
|
89
100
|
});
|
|
90
101
|
}
|
|
91
102
|
}
|
|
92
|
-
async sessions(directory, limit = 100) {
|
|
103
|
+
async sessions(directory, limit = 100, signal) {
|
|
104
|
+
signal?.throwIfAborted();
|
|
93
105
|
const client = await this.requireClient();
|
|
94
|
-
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
return unwrap(await client.session.list({ directory, limit }, { signal: signal ?? null }));
|
|
95
108
|
}
|
|
96
109
|
async session(directory, sessionID) {
|
|
97
110
|
const client = await this.requireClient();
|
|
@@ -238,11 +251,17 @@ export class OpenCodeServerClient {
|
|
|
238
251
|
}
|
|
239
252
|
}
|
|
240
253
|
async stop() {
|
|
254
|
+
this.startingController?.abort(new Error('OPENCODE_SERVER_STOPPED'));
|
|
255
|
+
const starting = this.starting;
|
|
241
256
|
this.client = undefined;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
await this.stopChild(this.child);
|
|
258
|
+
if (starting !== undefined)
|
|
259
|
+
await Promise.allSettled([starting]);
|
|
260
|
+
}
|
|
261
|
+
async stopChild(child) {
|
|
262
|
+
if (child === undefined || this.child !== child)
|
|
245
263
|
return;
|
|
264
|
+
this.child = undefined;
|
|
246
265
|
if (child.pid === undefined) {
|
|
247
266
|
child.kill('SIGTERM');
|
|
248
267
|
return;
|
|
@@ -280,24 +299,30 @@ async function availablePort() {
|
|
|
280
299
|
});
|
|
281
300
|
});
|
|
282
301
|
}
|
|
283
|
-
async function waitUntilHealthy(url, authorization, child, timeoutMs) {
|
|
302
|
+
async function waitUntilHealthy(url, authorization, child, timeoutMs, signal) {
|
|
284
303
|
const deadline = Date.now() + timeoutMs;
|
|
285
304
|
while (Date.now() < deadline) {
|
|
305
|
+
signal.throwIfAborted();
|
|
286
306
|
if (child.exitCode !== null)
|
|
287
307
|
throw new Error('OPENCODE_SERVER_EXITED');
|
|
288
308
|
try {
|
|
289
309
|
const response = await fetch(url, {
|
|
290
310
|
headers: { authorization },
|
|
291
|
-
signal: AbortSignal.
|
|
311
|
+
signal: AbortSignal.any([
|
|
312
|
+
signal,
|
|
313
|
+
AbortSignal.timeout(Math.min(1_000, Math.max(1, deadline - Date.now())))
|
|
314
|
+
])
|
|
292
315
|
});
|
|
293
316
|
if (response.ok)
|
|
294
317
|
return;
|
|
295
318
|
}
|
|
296
319
|
catch {
|
|
320
|
+
signal.throwIfAborted();
|
|
297
321
|
// The child may still be binding its loopback socket.
|
|
298
322
|
}
|
|
299
323
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
300
324
|
}
|
|
325
|
+
signal.throwIfAborted();
|
|
301
326
|
throw new Error('OPENCODE_SERVER_START_TIMEOUT');
|
|
302
327
|
}
|
|
303
328
|
function unwrap(result) {
|
|
@@ -196,15 +196,18 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
196
196
|
async discoverSessions(context, workspace) {
|
|
197
197
|
const result = [];
|
|
198
198
|
for (const target of workspace === undefined ? context.workspaces : [workspace]) {
|
|
199
|
+
context.signal?.throwIfAborted();
|
|
199
200
|
let sessions;
|
|
200
201
|
try {
|
|
201
|
-
sessions = await this.client.sessions(target.path);
|
|
202
|
+
sessions = await this.client.sessions(target.path, 100, context.signal);
|
|
202
203
|
}
|
|
203
204
|
catch (error) {
|
|
205
|
+
context.signal?.throwIfAborted();
|
|
204
206
|
if (context.cleanup)
|
|
205
207
|
throw error;
|
|
206
208
|
continue;
|
|
207
209
|
}
|
|
210
|
+
context.signal?.throwIfAborted();
|
|
208
211
|
for (const value of sessions) {
|
|
209
212
|
const session = openCodeSession(value);
|
|
210
213
|
if (session === undefined ||
|
|
@@ -213,7 +216,8 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
213
216
|
continue;
|
|
214
217
|
const title = context.cleanup
|
|
215
218
|
? { title: session.title, titleOrigin: 'OFFICIAL' }
|
|
216
|
-
: await this.sessionTitle(session);
|
|
219
|
+
: await this.sessionTitle(session, context.signal);
|
|
220
|
+
context.signal?.throwIfAborted();
|
|
217
221
|
result.push(context.createProjection({
|
|
218
222
|
workspace: target,
|
|
219
223
|
runner: this.name,
|
|
@@ -237,16 +241,18 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
237
241
|
return undefined;
|
|
238
242
|
return (await this.sessionTitle(native)).title;
|
|
239
243
|
}
|
|
240
|
-
async sessionTitle(session) {
|
|
244
|
+
async sessionTitle(session, signal) {
|
|
245
|
+
signal?.throwIfAborted();
|
|
241
246
|
const nativeTitle = session.title.trim();
|
|
242
247
|
if (nativeTitle.length === 0 ||
|
|
243
248
|
/^(?:new|untitled) session(?:\s*[-–—:]\s*\d{4}-\d{2}-\d{2}.*)?$/iu.test(nativeTitle)) {
|
|
244
249
|
try {
|
|
245
|
-
const preview = await this.firstUserMessagePreview(session);
|
|
250
|
+
const preview = await this.firstUserMessagePreview(session, signal);
|
|
246
251
|
if (preview)
|
|
247
252
|
return { title: preview, titleOrigin: 'PREVIEW' };
|
|
248
253
|
}
|
|
249
254
|
catch (error) {
|
|
255
|
+
signal?.throwIfAborted();
|
|
250
256
|
nodeLog('runner.opencode.title.failed', {
|
|
251
257
|
sessionId: session.id,
|
|
252
258
|
stage: 'preview',
|
|
@@ -256,10 +262,12 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
256
262
|
}
|
|
257
263
|
return { title: session.title, titleOrigin: 'OFFICIAL' };
|
|
258
264
|
}
|
|
259
|
-
async firstUserMessagePreview(session) {
|
|
265
|
+
async firstUserMessagePreview(session, signal) {
|
|
260
266
|
const key = `${session.directory}\0${session.id}`;
|
|
261
267
|
const cached = this.titlePreviews.get(key);
|
|
268
|
+
// Only in-flight reads share a cancellation owner; completed values retain their cache lifetime.
|
|
262
269
|
if (cached !== undefined &&
|
|
270
|
+
(cached.fulfilled || cached.signal === signal) &&
|
|
263
271
|
cached.updatedAt === session.updatedAt &&
|
|
264
272
|
cached.expiresAt > Date.now())
|
|
265
273
|
return cached.result;
|
|
@@ -267,6 +275,8 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
267
275
|
if (this.titlePreviews.size >= MAX_TITLE_PREVIEWS)
|
|
268
276
|
this.titlePreviews.delete(this.titlePreviews.keys().next().value);
|
|
269
277
|
const entry = {
|
|
278
|
+
signal,
|
|
279
|
+
fulfilled: false,
|
|
270
280
|
updatedAt: session.updatedAt,
|
|
271
281
|
expiresAt: Date.now() + TITLE_PREVIEW_TTL_MS,
|
|
272
282
|
result: (async () => {
|
|
@@ -274,10 +284,12 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
274
284
|
let preview;
|
|
275
285
|
const cursors = new Set();
|
|
276
286
|
for (;;) {
|
|
287
|
+
signal?.throwIfAborted();
|
|
277
288
|
const page = await this.client.messagePage(session.directory, session.id, {
|
|
278
289
|
limit: 100,
|
|
279
290
|
...(before === undefined ? {} : { before })
|
|
280
|
-
});
|
|
291
|
+
}, signal);
|
|
292
|
+
signal?.throwIfAborted();
|
|
281
293
|
preview = openCodeUserMessagePreview(page.messages) ?? preview;
|
|
282
294
|
if (page.nextCursor === null)
|
|
283
295
|
return preview;
|
|
@@ -290,7 +302,9 @@ export class OpenCodeRunner extends AbstractRunner {
|
|
|
290
302
|
};
|
|
291
303
|
this.titlePreviews.set(key, entry);
|
|
292
304
|
try {
|
|
293
|
-
|
|
305
|
+
const preview = await entry.result;
|
|
306
|
+
entry.fulfilled = true;
|
|
307
|
+
return preview;
|
|
294
308
|
}
|
|
295
309
|
catch (error) {
|
|
296
310
|
if (this.titlePreviews.get(key) === entry)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { agentObservationFromRunEvent } from './agent-network-observations.js';
|
|
2
|
+
import { sessionPendingApprovals } from './session-recent-activity.js';
|
|
3
|
+
/** Projects normalized Run events and owns transient observers from attachment through terminal cleanup.
|
|
4
|
+
* Run-keyed handles survive Session identity promotion without borrowing a replacement's lifetime.
|
|
5
|
+
* Runner-owned context observers remain with the Runner; this owner only borrows their delivery port.
|
|
6
|
+
*/
|
|
7
|
+
export class AgentNetworkRunProjection {
|
|
8
|
+
runtime;
|
|
9
|
+
observations;
|
|
10
|
+
transient = new Map();
|
|
11
|
+
constructor(runtime, observations) {
|
|
12
|
+
this.runtime = runtime;
|
|
13
|
+
this.observations = observations;
|
|
14
|
+
}
|
|
15
|
+
ensureObservations = (actor) => {
|
|
16
|
+
const existing = this.observations.get(actor.observationId ?? actor.sessionId);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const run = actor.runId === undefined ? undefined : this.runtime.getRun(actor.runId);
|
|
20
|
+
if (!run ||
|
|
21
|
+
run.sessionId !== actor.sessionId ||
|
|
22
|
+
this.runtime.activeSessionRun(actor.sessionId)?.id !== run.id)
|
|
23
|
+
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
24
|
+
const observer = this.observations.attach(actor.sessionId);
|
|
25
|
+
this.transient.set(run.id, observer);
|
|
26
|
+
return observer;
|
|
27
|
+
};
|
|
28
|
+
accept = ({ run, executionId, eventType, payload, terminalStatus }) => {
|
|
29
|
+
const child = this.runtime.getAgentSession(run.sessionId);
|
|
30
|
+
if (!child)
|
|
31
|
+
return;
|
|
32
|
+
const parent = child.parentSessionId == null
|
|
33
|
+
? undefined
|
|
34
|
+
: this.runtime.getAgentSession(child.parentSessionId);
|
|
35
|
+
let observation = agentObservationFromRunEvent({ eventType, payload, terminalStatus });
|
|
36
|
+
if (eventType === 'approval.request') {
|
|
37
|
+
const approvals = sessionPendingApprovals(this.runtime, child.id).pendingApprovals;
|
|
38
|
+
if (approvals.length)
|
|
39
|
+
observation = { kind: 'message', value: JSON.stringify({ pendingApprovals: approvals }) };
|
|
40
|
+
}
|
|
41
|
+
if (observation)
|
|
42
|
+
this.observations.publishChild({
|
|
43
|
+
...(parent?.workspaceId === child.workspaceId ? { parentId: parent.id } : {}),
|
|
44
|
+
childId: child.id,
|
|
45
|
+
runId: run.id,
|
|
46
|
+
executionId,
|
|
47
|
+
...observation
|
|
48
|
+
});
|
|
49
|
+
if (terminalStatus !== undefined) {
|
|
50
|
+
const observer = this.transient.get(run.id);
|
|
51
|
+
this.transient.delete(run.id);
|
|
52
|
+
observer?.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
dispose() {
|
|
56
|
+
for (const observer of this.transient.values())
|
|
57
|
+
observer.close();
|
|
58
|
+
this.transient.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -67,7 +67,7 @@ export async function validateComposerSkills(input, workspacePath, home = homedi
|
|
|
67
67
|
const directories = new SkillDirectoryService();
|
|
68
68
|
const [node, workspace] = await Promise.all([
|
|
69
69
|
directories.inspectNodeHome(home),
|
|
70
|
-
directories.inspectWorkspace(workspacePath)
|
|
70
|
+
directories.inspectWorkspace(workspacePath, true, { includeClaudeCompatibility: false })
|
|
71
71
|
]);
|
|
72
72
|
const effective = new Map(node.skills.map((skill) => [skill.name, { ...skill, source: 'NODE_HOME' }]));
|
|
73
73
|
for (const skill of workspace.skills)
|
|
@@ -76,7 +76,7 @@ function activity(runtime, sessionId) {
|
|
|
76
76
|
}
|
|
77
77
|
export function marctlSessionProgress(runtime, sessionId) {
|
|
78
78
|
const { latestActivity: _latest, ...progress } = activity(runtime, sessionId);
|
|
79
|
-
return { ...progress, ...
|
|
79
|
+
return { ...progress, ...sessionPendingApprovals(runtime, sessionId) };
|
|
80
80
|
}
|
|
81
81
|
export function sessionActivityStatus(runtime, sessionId) {
|
|
82
82
|
const run = runtime.activeSessionRun(sessionId);
|
|
@@ -100,7 +100,7 @@ function isSleepingTool(item) {
|
|
|
100
100
|
typeof presentation.startedAt === 'string');
|
|
101
101
|
}
|
|
102
102
|
/** Summarizes current approval facts without exposing tool input or native payloads. */
|
|
103
|
-
function
|
|
103
|
+
export function sessionPendingApprovals(runtime, sessionId) {
|
|
104
104
|
const run = runtime.activeSessionRun(sessionId);
|
|
105
105
|
const executionId = run && runtime.currentExecutionId(run.id);
|
|
106
106
|
const approvals = run && !isTerminalRunStatus(run.status)
|
|
@@ -150,7 +150,7 @@ export function marctlSessionDetail(runtime, session, queuedCount) {
|
|
|
150
150
|
queuedCount,
|
|
151
151
|
latestMessage,
|
|
152
152
|
recentTools,
|
|
153
|
-
...
|
|
153
|
+
...sessionPendingApprovals(runtime, session.id)
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
/** Agent state views omit execution identities; low-level CLI queries keep their own contract. */
|
|
@@ -5,25 +5,27 @@ import { hasBrokenWorkspaceSkillGitExclude } from '../workspace/workspace-git-ex
|
|
|
5
5
|
const MAX_METADATA_BYTES = 64 * 1024;
|
|
6
6
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
7
7
|
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
|
|
8
|
+
/** Owns filesystem Skill inspection; effective lists exclude installation compatibility copies. */
|
|
8
9
|
export class SkillDirectoryService {
|
|
9
|
-
async inspectNodeHome(home
|
|
10
|
+
async inspectNodeHome(home) {
|
|
10
11
|
const agentsRoot = path.join(home, '.agents', 'skills');
|
|
11
12
|
const claudeRoot = path.join(home, '.claude', 'skills');
|
|
12
13
|
const compatibilityMode = await nodeCompatibilityMode(agentsRoot, claudeRoot);
|
|
13
14
|
const agents = await scanRoot(agentsRoot, 'AGENTS');
|
|
14
|
-
const claude = options.includeClaudeCompatibility === true && compatibilityMode !== 'UNAVAILABLE'
|
|
15
|
-
? await scanRoot(claudeRoot, 'CLAUDE')
|
|
16
|
-
: [];
|
|
17
15
|
return {
|
|
18
16
|
targetKind: 'NODE',
|
|
19
17
|
compatibilityMode,
|
|
20
|
-
skills:
|
|
21
|
-
? mergeNodeCompatibilityRoots(agents, claude)
|
|
22
|
-
: agents.map(withoutSource)
|
|
18
|
+
skills: agents.map(withoutSource)
|
|
23
19
|
};
|
|
24
20
|
}
|
|
25
|
-
async inspectWorkspace(workspace, includeParentRepository = true) {
|
|
21
|
+
async inspectWorkspace(workspace, includeParentRepository = true, options = {}) {
|
|
26
22
|
const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
|
|
23
|
+
if (options.includeClaudeCompatibility === false)
|
|
24
|
+
return {
|
|
25
|
+
targetKind: 'WORKSPACE',
|
|
26
|
+
compatibilityMode: 'MANAGED_COPY',
|
|
27
|
+
skills: agents.map(withoutSource)
|
|
28
|
+
};
|
|
27
29
|
const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
|
|
28
30
|
const skills = mergeWorkspaceCopies(agents, claude);
|
|
29
31
|
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name), includeParentRepository);
|
|
@@ -181,29 +183,6 @@ function mergeWorkspaceCopies(agents, claude) {
|
|
|
181
183
|
return withoutSource(copies.agents);
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
|
-
function mergeNodeCompatibilityRoots(agents, claude) {
|
|
185
|
-
const byName = new Map();
|
|
186
|
-
for (const item of agents)
|
|
187
|
-
byName.set(item.name, { agents: item });
|
|
188
|
-
for (const item of claude)
|
|
189
|
-
byName.set(item.name, { ...byName.get(item.name), claude: item });
|
|
190
|
-
return [...byName.entries()]
|
|
191
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
192
|
-
.map(([name, copies]) => {
|
|
193
|
-
if (copies.agents === undefined || copies.claude === undefined)
|
|
194
|
-
return withoutSource((copies.agents ?? copies.claude));
|
|
195
|
-
if (copies.agents.description !== copies.claude.description ||
|
|
196
|
-
copies.agents.localStatus !== copies.claude.localStatus ||
|
|
197
|
-
JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
|
|
198
|
-
return {
|
|
199
|
-
name,
|
|
200
|
-
description: copies.agents.description,
|
|
201
|
-
localStatus: 'PARTIAL',
|
|
202
|
-
skillFilePath: copies.agents.skillFilePath
|
|
203
|
-
};
|
|
204
|
-
return withoutSource(copies.agents);
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
186
|
function withoutSource(entry) {
|
|
208
187
|
return {
|
|
209
188
|
name: entry.name,
|
|
@@ -17,18 +17,16 @@ export class SkillNodeOperationService {
|
|
|
17
17
|
}
|
|
18
18
|
operations() {
|
|
19
19
|
return {
|
|
20
|
-
'node.skills.inspect': async (
|
|
20
|
+
'node.skills.inspect': async (_data) => {
|
|
21
21
|
await this.installer.recoverNodeHome(homedir());
|
|
22
|
-
return this.directories.inspectNodeHome(homedir()
|
|
23
|
-
includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] === true
|
|
24
|
-
});
|
|
22
|
+
return this.directories.inspectNodeHome(homedir());
|
|
25
23
|
},
|
|
26
24
|
'node.skills.install': (data) => this.install('NODE', data),
|
|
27
25
|
'node.skills.remove': (data) => this.remove('NODE', data),
|
|
28
26
|
'workspace.skills.inspect': async (data) => {
|
|
29
27
|
const workspace = this.workspacePath(record(data)['workspaceId']);
|
|
30
28
|
await this.installer.recoverWorkspace(workspace);
|
|
31
|
-
return this.directories.inspectWorkspace(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
29
|
+
return this.directories.inspectWorkspace(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true, { includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] !== false });
|
|
32
30
|
},
|
|
33
31
|
'workspace.skills.install': (data) => this.install('WORKSPACE', data),
|
|
34
32
|
'workspace.skills.remove': (data) => this.remove('WORKSPACE', data),
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { deduplicateDirectSessions } from './runner-native-session-parsers.js';
|
|
2
|
+
/** Owns native identity/metadata projection, not Runner execution.
|
|
3
|
+
* Discovery borrows its caller's lifetime; cancellation revokes database projection before
|
|
4
|
+
* releasing the wait on a third-party read that may not itself support interruption.
|
|
5
|
+
*/
|
|
2
6
|
export class NativeSessionProjectionService {
|
|
3
7
|
options;
|
|
4
8
|
directNative = new Map();
|
|
@@ -8,7 +12,8 @@ export class NativeSessionProjectionService {
|
|
|
8
12
|
constructor(options) {
|
|
9
13
|
this.options = options;
|
|
10
14
|
}
|
|
11
|
-
async discover(runnerName, workspaceId, cleanup = false) {
|
|
15
|
+
async discover(runnerName, workspaceId, cleanup = false, signal) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
12
17
|
const database = this.requireDatabase();
|
|
13
18
|
const workspace = workspaceId === undefined ? undefined : database.getWorkspace(workspaceId);
|
|
14
19
|
if (workspaceId !== undefined && workspace === undefined)
|
|
@@ -16,11 +21,16 @@ export class NativeSessionProjectionService {
|
|
|
16
21
|
const runner = this.options.runners.product(runnerName);
|
|
17
22
|
if (runner === undefined || !runner.available(this.options.capabilities()))
|
|
18
23
|
throw new Error('RUNNER_UNAVAILABLE');
|
|
19
|
-
const sessions = await runner.discoverSessions({
|
|
24
|
+
const sessions = await awaitDiscovery(runner.discoverSessions({
|
|
25
|
+
...(signal === undefined ? {} : { signal }),
|
|
20
26
|
...(cleanup ? { cleanup: true } : {}),
|
|
21
27
|
workspaces: database.listWorkspaces(),
|
|
22
|
-
createProjection: (input) =>
|
|
23
|
-
|
|
28
|
+
createProjection: (input) => {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
return this.project(input);
|
|
31
|
+
}
|
|
32
|
+
}, workspace), signal);
|
|
33
|
+
signal?.throwIfAborted();
|
|
24
34
|
const ordered = deduplicateDirectSessions(sessions);
|
|
25
35
|
if (!cleanup) {
|
|
26
36
|
this.remember(ordered);
|
|
@@ -57,16 +67,19 @@ export class NativeSessionProjectionService {
|
|
|
57
67
|
this.remember(sessions);
|
|
58
68
|
return sessions.map(this.options.present);
|
|
59
69
|
}
|
|
60
|
-
async discoverWorkspace(workspaceId, onRunnerDiscovered, cleanup = false) {
|
|
70
|
+
async discoverWorkspace(workspaceId, onRunnerDiscovered, cleanup = false, signal) {
|
|
71
|
+
signal?.throwIfAborted();
|
|
61
72
|
const discovered = await Promise.allSettled(this.options.runners
|
|
62
73
|
.advertised()
|
|
63
74
|
.filter((runner) => runner.available(this.options.capabilities()))
|
|
64
75
|
.map(async (runner) => {
|
|
65
76
|
const name = runner.name;
|
|
66
|
-
const sessions = await this.discover(name, workspaceId, cleanup);
|
|
77
|
+
const sessions = await this.discover(name, workspaceId, cleanup, signal);
|
|
78
|
+
signal?.throwIfAborted();
|
|
67
79
|
onRunnerDiscovered?.(name, sessions);
|
|
68
80
|
return sessions;
|
|
69
81
|
}));
|
|
82
|
+
signal?.throwIfAborted();
|
|
70
83
|
const failed = discovered.find((result) => result.status === 'rejected');
|
|
71
84
|
if (failed?.status === 'rejected')
|
|
72
85
|
throw failed.reason;
|
|
@@ -435,6 +448,28 @@ export class NativeSessionProjectionService {
|
|
|
435
448
|
return database;
|
|
436
449
|
}
|
|
437
450
|
}
|
|
451
|
+
/** The callback capability is revoked by discover(); late SDK outcomes are still consumed. */
|
|
452
|
+
function awaitDiscovery(read, signal) {
|
|
453
|
+
if (signal === undefined)
|
|
454
|
+
return read;
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const abort = () => {
|
|
457
|
+
signal.removeEventListener('abort', abort);
|
|
458
|
+
reject(signal.reason);
|
|
459
|
+
};
|
|
460
|
+
read.then((sessions) => {
|
|
461
|
+
signal.removeEventListener('abort', abort);
|
|
462
|
+
resolve(sessions);
|
|
463
|
+
}, (error) => {
|
|
464
|
+
signal.removeEventListener('abort', abort);
|
|
465
|
+
reject(error);
|
|
466
|
+
});
|
|
467
|
+
if (signal.aborted)
|
|
468
|
+
abort();
|
|
469
|
+
else
|
|
470
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
438
473
|
function sessionIdDistance(left, right, maximum) {
|
|
439
474
|
if (Math.abs(left.length - right.length) > maximum)
|
|
440
475
|
return maximum + 1;
|
|
@@ -7,12 +7,23 @@ import { sessionPage } from './pagination.js';
|
|
|
7
7
|
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
8
8
|
import { marctlSessionSummary, sessionActivityStatus } from '../service/session-recent-activity.js';
|
|
9
9
|
import { WorkspaceSessionCleanupBudget, WorkspaceSessionCleanupDeadline } from '../service/workspace-session-cleanup-budget.js';
|
|
10
|
+
/** Owns catalog queries and Session catalog mutations; borrows the Workspace native cache.
|
|
11
|
+
* Public operations and internal observation share query rules, not transport/admission routing.
|
|
12
|
+
*/
|
|
10
13
|
export class SessionCatalogService {
|
|
11
14
|
options;
|
|
12
15
|
rewindQueue = Promise.resolve();
|
|
13
16
|
constructor(options) {
|
|
14
17
|
this.options = options;
|
|
15
18
|
}
|
|
19
|
+
/** Internal observation uses the same catalog query without public operation dispatch. */
|
|
20
|
+
listWorkspacePage(workspaceId, sessionGroup) {
|
|
21
|
+
return this.list({
|
|
22
|
+
workspaceId,
|
|
23
|
+
limit: 30,
|
|
24
|
+
...(sessionGroup === undefined ? {} : { sessionGroup })
|
|
25
|
+
});
|
|
26
|
+
}
|
|
16
27
|
operations() {
|
|
17
28
|
return {
|
|
18
29
|
'session.list': (data) => this.list(record(data)),
|