@myagentroam/node 0.9.0 → 0.9.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.d.ts +2 -0
- package/dist/connector.js +38 -12
- package/dist/native-session-history.js +72 -14
- package/dist/runner/abstract-runner.d.ts +1 -1
- package/dist/runner/abstract-runner.js +2 -2
- package/dist/runner/claude/managed-run-controller.js +17 -7
- package/dist/runner/claude-code-runner.js +3 -4
- package/dist/runner/codex/conversation-parser.d.ts +2 -2
- package/dist/runner/codex/conversation-parser.js +30 -12
- package/dist/runner/codex/managed-run-controller.js +7 -6
- package/dist/runner/codex-runner.d.ts +1 -1
- package/dist/runner/codex-runner.js +10 -8
- package/dist/runner/opencode/conversation-parser.d.ts +11 -7
- package/dist/runner/opencode/conversation-parser.js +15 -15
- package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
- package/dist/runner/opencode/managed-run-controller.js +26 -12
- package/dist/runner-command-engine.js +1 -1
- package/dist/runner-profiles.js +25 -17
- package/dist/runner-usage.js +5 -5
- package/dist/service/conversation-history-service.js +55 -24
- package/dist/service/conversation-segment-service.d.ts +20 -0
- package/dist/service/conversation-segment-service.js +155 -0
- package/dist/service/mcp-installation-verifier.js +1 -1
- package/dist/service/native-session-watch-service.d.ts +1 -0
- package/dist/service/native-session-watch-service.js +21 -4
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/run-attachment-service.d.ts +3 -2
- package/dist/service/run-attachment-service.js +21 -11
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +8 -2
- package/dist/service/session-catalog-service.js +4 -4
- package/dist/service/session-presentation-service.d.ts +1 -0
- package/dist/service/session-presentation-service.js +8 -1
- package/dist/service/session-query-service.d.ts +4 -0
- package/dist/service/session-query-service.js +6 -1
- package/dist/service/skill-directory-service.js +3 -26
- package/dist/service/skill-install-service.js +4 -74
- package/dist/service/skill-node-operation-service.js +2 -1
- package/dist/service/workspace-git-exclude-service.d.ts +4 -0
- package/dist/service/workspace-git-exclude-service.js +119 -0
- package/dist/service/workspace-queue-workbench-service.js +3 -2
- package/dist/service/workspace-service.js +2 -0
- package/dist/supervisor.js +2 -1
- package/dist/terminal.js +1 -1
- package/dist/util/node-operation-parsers.js +2 -2
- package/dist/util/runner-native-session-parsers.d.ts +2 -3
- package/dist/util/runner-native-session-parsers.js +5 -14
- package/dist/util/safe-error.d.ts +2 -0
- package/dist/util/safe-error.js +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const TURN_PAGE_SIZE = 30;
|
|
2
|
+
export function conversationSegments(turn) {
|
|
3
|
+
const groups = [];
|
|
4
|
+
let current = [];
|
|
5
|
+
for (const item of turn.items) {
|
|
6
|
+
current.push(item);
|
|
7
|
+
if (item.kind !== 'assistant_message')
|
|
8
|
+
continue;
|
|
9
|
+
groups.push(current);
|
|
10
|
+
current = [];
|
|
11
|
+
}
|
|
12
|
+
if (current.length > 0 || groups.length === 0)
|
|
13
|
+
groups.push(current);
|
|
14
|
+
return groups.map((items, index) => {
|
|
15
|
+
const id = `${turn.id}:segment:${index}`;
|
|
16
|
+
const latest = index === groups.length - 1;
|
|
17
|
+
const projectedItems = items.map((item) => ({
|
|
18
|
+
...item,
|
|
19
|
+
segmentId: id,
|
|
20
|
+
segmentIndex: index
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
sessionId: turn.sessionId,
|
|
25
|
+
turnId: turn.id,
|
|
26
|
+
runId: turn.runId,
|
|
27
|
+
index,
|
|
28
|
+
status: (latest ? turn.status : 'SUCCEEDED'),
|
|
29
|
+
...(latest && turn.rewind !== undefined ? { rewind: turn.rewind } : {}),
|
|
30
|
+
items: projectedItems,
|
|
31
|
+
startedAt: projectedItems[0]?.startedAt ?? turn.startedAt,
|
|
32
|
+
completedAt: projectedItems.at(-1)?.completedAt ?? null
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
|
|
37
|
+
let index = 0;
|
|
38
|
+
const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
|
|
39
|
+
for (const item of turn.items) {
|
|
40
|
+
if (item.id === itemId || item.id === runtimeItemId)
|
|
41
|
+
return { id: `${turn.id}:segment:${index}`, index };
|
|
42
|
+
if (item.kind === 'assistant_message')
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
if (itemKind === 'assistant_message' && turn.items.at(-1)?.kind === 'assistant_message')
|
|
46
|
+
index -= 1;
|
|
47
|
+
return { id: `${turn.id}:segment:${index}`, index };
|
|
48
|
+
}
|
|
49
|
+
export class ConversationSegmentService {
|
|
50
|
+
readTurns;
|
|
51
|
+
constructor(readTurns) {
|
|
52
|
+
this.readTurns = readTurns;
|
|
53
|
+
}
|
|
54
|
+
projectInitial(session, page, limit) {
|
|
55
|
+
const all = page.turns.flatMap(conversationSegments);
|
|
56
|
+
const segments = all.slice(-limit);
|
|
57
|
+
return {
|
|
58
|
+
snapshotSequence: page.snapshotSequence,
|
|
59
|
+
source: page.source,
|
|
60
|
+
readAt: page.readAt,
|
|
61
|
+
latestVisibleAt: page.latestVisibleAt,
|
|
62
|
+
unit: 'SEGMENT',
|
|
63
|
+
segments,
|
|
64
|
+
nextCursor: segments.length === 0 || (segments.length === all.length && page.nextCursor === null)
|
|
65
|
+
? null
|
|
66
|
+
: encodeCursor({
|
|
67
|
+
version: 1,
|
|
68
|
+
sessionId: session.id,
|
|
69
|
+
beforeSegmentId: segments[0].id
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async read(session, input) {
|
|
74
|
+
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
75
|
+
const boundary = input.cursor === undefined ? undefined : decodeCursor(input.cursor, session.id);
|
|
76
|
+
const collected = [];
|
|
77
|
+
const visited = new Set();
|
|
78
|
+
let turnCursor;
|
|
79
|
+
let locatingBoundary = boundary !== undefined;
|
|
80
|
+
let hasOlder = false;
|
|
81
|
+
let metadata;
|
|
82
|
+
while (collected.length < limit) {
|
|
83
|
+
const page = await this.readTurns(session, {
|
|
84
|
+
...(turnCursor === undefined ? {} : { cursor: turnCursor }),
|
|
85
|
+
limit: TURN_PAGE_SIZE
|
|
86
|
+
});
|
|
87
|
+
metadata ??= {
|
|
88
|
+
snapshotSequence: page.snapshotSequence,
|
|
89
|
+
source: page.source,
|
|
90
|
+
readAt: page.readAt,
|
|
91
|
+
latestVisibleAt: page.latestVisibleAt
|
|
92
|
+
};
|
|
93
|
+
const pageSegments = page.turns.flatMap(conversationSegments);
|
|
94
|
+
let eligible = pageSegments;
|
|
95
|
+
if (locatingBoundary) {
|
|
96
|
+
const boundaryIndex = pageSegments.findIndex((segment) => segment.id === boundary?.beforeSegmentId);
|
|
97
|
+
if (boundaryIndex >= 0) {
|
|
98
|
+
eligible = pageSegments.slice(0, boundaryIndex);
|
|
99
|
+
locatingBoundary = false;
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
eligible = [];
|
|
103
|
+
}
|
|
104
|
+
if (!locatingBoundary && eligible.length > 0) {
|
|
105
|
+
const remaining = Math.max(0, limit - collected.length);
|
|
106
|
+
const take = eligible.slice(-remaining);
|
|
107
|
+
hasOlder = eligible.length > take.length || page.nextCursor !== null;
|
|
108
|
+
collected.unshift(...take);
|
|
109
|
+
}
|
|
110
|
+
if (collected.length >= limit || page.nextCursor === null)
|
|
111
|
+
break;
|
|
112
|
+
if (visited.has(page.nextCursor))
|
|
113
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
114
|
+
visited.add(page.nextCursor);
|
|
115
|
+
turnCursor = page.nextCursor;
|
|
116
|
+
}
|
|
117
|
+
if (locatingBoundary)
|
|
118
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
119
|
+
const nextCursor = collected.length === 0 || !hasOlder
|
|
120
|
+
? null
|
|
121
|
+
: encodeCursor({
|
|
122
|
+
version: 1,
|
|
123
|
+
sessionId: session.id,
|
|
124
|
+
beforeSegmentId: collected[0].id
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
...(metadata ?? {
|
|
128
|
+
snapshotSequence: 0,
|
|
129
|
+
source: 'runtime',
|
|
130
|
+
readAt: Date.now(),
|
|
131
|
+
latestVisibleAt: 0
|
|
132
|
+
}),
|
|
133
|
+
unit: 'SEGMENT',
|
|
134
|
+
segments: collected,
|
|
135
|
+
nextCursor
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function encodeCursor(cursor) {
|
|
140
|
+
return Buffer.from(JSON.stringify(cursor)).toString('base64url');
|
|
141
|
+
}
|
|
142
|
+
function decodeCursor(value, sessionId) {
|
|
143
|
+
try {
|
|
144
|
+
const decoded = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
145
|
+
if (decoded.version !== 1 ||
|
|
146
|
+
decoded.sessionId !== sessionId ||
|
|
147
|
+
typeof decoded.beforeSegmentId !== 'string' ||
|
|
148
|
+
decoded.beforeSegmentId.length === 0)
|
|
149
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
150
|
+
return decoded;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -4,7 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
const INSTALL_TIMEOUT_MS = 90_000;
|
|
5
5
|
export class McpInstallationVerifier {
|
|
6
6
|
async verify(input) {
|
|
7
|
-
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.
|
|
7
|
+
const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.1' });
|
|
8
8
|
try {
|
|
9
9
|
await withTimeout((async () => {
|
|
10
10
|
if (input.entry.runtime.transport === 'STDIO') {
|
|
@@ -8,6 +8,7 @@ export interface NativeSessionWatchOptions<TPage extends {
|
|
|
8
8
|
readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
|
|
9
9
|
readonly present: (session: NodeAgentSession) => unknown;
|
|
10
10
|
readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
|
|
11
|
+
readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
|
|
11
12
|
readonly emitSession: (session: unknown) => void;
|
|
12
13
|
readonly emitTurn: (turn: unknown) => void;
|
|
13
14
|
}
|
|
@@ -15,7 +15,12 @@ export class NativeSessionWatchService {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
async executeWatch(input) {
|
|
18
|
-
|
|
18
|
+
const projectInitialPage = this.options.projectInitialPage;
|
|
19
|
+
if (input.unit === 'SEGMENT' && projectInitialPage === undefined)
|
|
20
|
+
throw new Error('SESSION_INVALID');
|
|
21
|
+
if (typeof input.sessionId !== 'string' ||
|
|
22
|
+
(input.mode !== 'initial' && input.mode !== 'renew') ||
|
|
23
|
+
(input.unit !== undefined && input.unit !== 'SEGMENT'))
|
|
19
24
|
throw new Error('SESSION_INVALID');
|
|
20
25
|
const session = await this.options.resolve(input.sessionId);
|
|
21
26
|
if (session === undefined || session.externalSessionId === null)
|
|
@@ -27,9 +32,21 @@ export class NativeSessionWatchService {
|
|
|
27
32
|
return { renewed: true };
|
|
28
33
|
}
|
|
29
34
|
const watch = this.watch(session);
|
|
30
|
-
const
|
|
31
|
-
if (watch !== undefined &&
|
|
35
|
+
const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
|
|
36
|
+
if (watch !== undefined && turnSnapshot === undefined) {
|
|
37
|
+
this.stop(session.id);
|
|
32
38
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
39
|
+
}
|
|
40
|
+
let snapshot = turnSnapshot;
|
|
41
|
+
if (turnSnapshot !== undefined && input.unit === 'SEGMENT') {
|
|
42
|
+
try {
|
|
43
|
+
snapshot = projectInitialPage(session, turnSnapshot, 'SEGMENT', 10);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.stop(session.id);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
return {
|
|
34
51
|
session: this.options.present(session),
|
|
35
52
|
watch: watch === undefined ? null : { expiresAt: watch.expiresAt },
|
|
@@ -131,7 +148,7 @@ export class NativeSessionWatchService {
|
|
|
131
148
|
}
|
|
132
149
|
const page = await this.options.readPage(session, limit);
|
|
133
150
|
watch.failures = 0;
|
|
134
|
-
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(
|
|
151
|
+
const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(-10);
|
|
135
152
|
const signature = JSON.stringify(turns);
|
|
136
153
|
if (signature === watch.signature)
|
|
137
154
|
return page;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nodeLog } from '../operational.js';
|
|
2
2
|
import { validUserAuthorization } from '../runner/codex/conversation-parser.js';
|
|
3
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
4
|
/** Validates and executes the closed Admin-facing Node operation envelope. */
|
|
4
5
|
export class NodeRequestService {
|
|
5
6
|
options;
|
|
@@ -44,7 +45,7 @@ export class NodeRequestService {
|
|
|
44
45
|
durationMs: Math.round(performance.now() - startedAt),
|
|
45
46
|
error: error instanceof Error ? error.message : 'INTERNAL_ERROR'
|
|
46
47
|
});
|
|
47
|
-
this.options.send('node.response', { error: error
|
|
48
|
+
this.options.send('node.response', { error: safeErrorCode(error, 'INTERNAL_ERROR') }, envelope.id);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -11,14 +11,15 @@ export interface NativeImageSource {
|
|
|
11
11
|
readonly name: string;
|
|
12
12
|
readonly mime: string;
|
|
13
13
|
readonly path: string;
|
|
14
|
+
readonly root: string;
|
|
14
15
|
}
|
|
15
16
|
export type RunnerImageAttachment = NodeMessageAttachmentInput & {
|
|
16
17
|
readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
17
18
|
};
|
|
18
19
|
export declare class RunAttachmentService {
|
|
19
|
-
private readonly
|
|
20
|
+
private readonly nativeImageRoots;
|
|
20
21
|
readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
|
|
21
|
-
constructor(
|
|
22
|
+
constructor(nativeImageRoots: (session: NodeAgentSession) => readonly string[]);
|
|
22
23
|
materializeCodex(runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly Extract<CodexComposerInput, {
|
|
23
24
|
readonly type: 'localImage';
|
|
24
25
|
}>[]>;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
-
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { AttachmentDomainState } from './attachment-domain-state.js';
|
|
5
5
|
const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
6
6
|
const IMAGE_CACHE_TTL_MS = 60 * 60_000;
|
|
7
7
|
export class RunAttachmentService {
|
|
8
|
-
|
|
8
|
+
nativeImageRoots;
|
|
9
9
|
state = new AttachmentDomainState();
|
|
10
|
-
constructor(
|
|
11
|
-
this.
|
|
10
|
+
constructor(nativeImageRoots) {
|
|
11
|
+
this.nativeImageRoots = nativeImageRoots;
|
|
12
12
|
}
|
|
13
13
|
async materializeCodex(runId, attachments) {
|
|
14
14
|
if (attachments.length === 0)
|
|
@@ -95,8 +95,8 @@ export class RunAttachmentService {
|
|
|
95
95
|
return cached.images[imageIndex];
|
|
96
96
|
}
|
|
97
97
|
registerNative(session, images) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
98
|
+
const roots = this.nativeImageRoots(session).map((root) => resolve(root));
|
|
99
|
+
if (roots.length === 0)
|
|
100
100
|
return [];
|
|
101
101
|
const runId = `native-history:${session.id}`;
|
|
102
102
|
const sources = this.state.nativeHistoryImages.get(runId) ?? [];
|
|
@@ -107,15 +107,18 @@ export class RunAttachmentService {
|
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
const path = resolve(image.localPath);
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const root = roots.find((candidate) => {
|
|
111
|
+
const relation = relative(candidate, path);
|
|
112
|
+
return relation.length > 0 && !relation.startsWith('..') && !isAbsolute(relation);
|
|
113
|
+
});
|
|
114
|
+
if (root === undefined) {
|
|
112
115
|
attachments.push({ name: image.name, mime: image.mime });
|
|
113
116
|
continue;
|
|
114
117
|
}
|
|
115
118
|
let imageIndex = sources.findIndex((source) => source.path === path);
|
|
116
119
|
if (imageIndex < 0) {
|
|
117
120
|
imageIndex = sources.length;
|
|
118
|
-
sources.push({ name: image.name, mime: image.mime, path });
|
|
121
|
+
sources.push({ name: image.name, mime: image.mime, path, root });
|
|
119
122
|
}
|
|
120
123
|
attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
|
|
121
124
|
}
|
|
@@ -128,10 +131,17 @@ export class RunAttachmentService {
|
|
|
128
131
|
if (source === undefined)
|
|
129
132
|
return undefined;
|
|
130
133
|
try {
|
|
131
|
-
const
|
|
134
|
+
const [actualRoot, actualPath] = await Promise.all([
|
|
135
|
+
realpath(source.root),
|
|
136
|
+
realpath(source.path)
|
|
137
|
+
]);
|
|
138
|
+
const relation = relative(actualRoot, actualPath);
|
|
139
|
+
if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation))
|
|
140
|
+
return undefined;
|
|
141
|
+
const info = await lstat(actualPath);
|
|
132
142
|
if (!info.isFile() || info.size <= 0 || info.size > MAX_ATTACHMENT_BYTES)
|
|
133
143
|
return undefined;
|
|
134
|
-
return { ...source, dataBase64: (await readFile(
|
|
144
|
+
return { ...source, dataBase64: (await readFile(actualPath)).toString('base64') };
|
|
135
145
|
}
|
|
136
146
|
catch {
|
|
137
147
|
return undefined;
|
|
@@ -25,6 +25,7 @@ export declare class RunEventService {
|
|
|
25
25
|
constructor(options: RunEventServiceOptions);
|
|
26
26
|
emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
|
|
27
27
|
clear(): void;
|
|
28
|
+
flushPendingText(runId: string): void;
|
|
28
29
|
private enqueueText;
|
|
29
30
|
private flushText;
|
|
30
31
|
private emitNow;
|
|
@@ -25,6 +25,9 @@ export class RunEventService {
|
|
|
25
25
|
clearTimeout(pending.timer);
|
|
26
26
|
this.pendingText.clear();
|
|
27
27
|
}
|
|
28
|
+
flushPendingText(runId) {
|
|
29
|
+
this.flushText(runId);
|
|
30
|
+
}
|
|
28
31
|
enqueueText(runId, text) {
|
|
29
32
|
if (text.length === 0)
|
|
30
33
|
return;
|
|
@@ -59,8 +62,9 @@ export class RunEventService {
|
|
|
59
62
|
nodeLog('run.event', { runId, eventType, status });
|
|
60
63
|
if (status === 'FAILED')
|
|
61
64
|
this.options.metrics.runnerFailed();
|
|
65
|
+
let persisted = undefined;
|
|
62
66
|
try {
|
|
63
|
-
this.options.runtime.appendRunEvent({
|
|
67
|
+
persisted = this.options.runtime.appendRunEvent({
|
|
64
68
|
runId,
|
|
65
69
|
sequence: event.sequence,
|
|
66
70
|
eventType,
|
|
@@ -84,7 +88,9 @@ export class RunEventService {
|
|
|
84
88
|
if (session !== undefined)
|
|
85
89
|
this.options.emitWorkbench('session', { session: this.options.presentSession(session) });
|
|
86
90
|
}
|
|
87
|
-
this.options.emitWorkbench('conversation', {
|
|
91
|
+
this.options.emitWorkbench('conversation', {
|
|
92
|
+
event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
|
|
93
|
+
});
|
|
88
94
|
if (status === undefined || !isTerminalRunStatus(status))
|
|
89
95
|
return;
|
|
90
96
|
const turn = this.options.runtime.conversationTurnForRun(runId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { removeNativeSession } from '../native-session-history.js';
|
|
2
2
|
import { isTerminalRunStatus, sessionPage } from '../util/node-operation-parsers.js';
|
|
3
|
+
import { latestSessionActivityAt } from './session-presentation-service.js';
|
|
3
4
|
export class SessionCatalogService {
|
|
4
5
|
options;
|
|
5
6
|
rewindQueue = Promise.resolve();
|
|
@@ -35,7 +36,7 @@ export class SessionCatalogService {
|
|
|
35
36
|
pinOrder: source.pinOrder ?? null,
|
|
36
37
|
externalDiscovered: true,
|
|
37
38
|
titleSource: session.customTitle === null ? source.titleSource : session.titleSource,
|
|
38
|
-
lastActivityAt: source.lastActivityAt
|
|
39
|
+
lastActivityAt: latestSessionActivityAt(session.lastActivityAt, source.lastActivityAt)
|
|
39
40
|
});
|
|
40
41
|
});
|
|
41
42
|
const merged = new Map([
|
|
@@ -100,8 +101,7 @@ export class SessionCatalogService {
|
|
|
100
101
|
delivery: resumed === undefined ? 'REJECTED' : 'RESUMABLE',
|
|
101
102
|
...(resumed === undefined
|
|
102
103
|
? {
|
|
103
|
-
reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
|
|
104
|
-
...(failure?.detail === undefined ? {} : { reasonDetail: failure.detail })
|
|
104
|
+
reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
|
|
105
105
|
}
|
|
106
106
|
: {})
|
|
107
107
|
};
|
|
@@ -130,7 +130,7 @@ export class SessionCatalogService {
|
|
|
130
130
|
const expected = input.position === 'BEFORE' ? turn?.rewind?.before : turn?.rewind?.after;
|
|
131
131
|
if (turn === undefined || expected !== input.boundary)
|
|
132
132
|
throw new Error('SESSION_REWIND_TARGET_INVALID');
|
|
133
|
-
const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? '
|
|
133
|
+
const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? 'Untitled session', this.options.runtime
|
|
134
134
|
.listAgentSessions()
|
|
135
135
|
.flatMap((candidate) => [candidate.customTitle ?? candidate.runnerTitle].filter(isString)));
|
|
136
136
|
const runner = this.options.runners.require(source.runner);
|
|
@@ -30,4 +30,5 @@ export declare class SessionPresentationService {
|
|
|
30
30
|
present(session: NodeAgentSession): PresentedSession;
|
|
31
31
|
presentDiscovered(sessions: readonly NodeAgentSession[]): readonly PresentedSession[];
|
|
32
32
|
}
|
|
33
|
+
export declare function latestSessionActivityAt(current: number, discovered: number): number;
|
|
33
34
|
export {};
|
|
@@ -30,7 +30,14 @@ export class SessionPresentationService {
|
|
|
30
30
|
const synchronized = session.runnerTitle === null
|
|
31
31
|
? projected
|
|
32
32
|
: this.options.runtime.setRunnerTitleFromNative(projected.id, session.runnerTitle);
|
|
33
|
-
return this.present({
|
|
33
|
+
return this.present({
|
|
34
|
+
...synchronized,
|
|
35
|
+
lastActivityAt: latestSessionActivityAt(synchronized.lastActivityAt, session.lastActivityAt)
|
|
36
|
+
});
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
export function latestSessionActivityAt(current, discovered) {
|
|
41
|
+
const known = [current, discovered].filter((value) => Number.isFinite(value) && value > 0);
|
|
42
|
+
return known.length === 0 ? 0 : Math.max(...known);
|
|
43
|
+
}
|
|
@@ -13,6 +13,10 @@ export interface SessionQueryServiceOptions {
|
|
|
13
13
|
readonly cursor?: string;
|
|
14
14
|
readonly limit?: number;
|
|
15
15
|
}) => Promise<unknown>;
|
|
16
|
+
readonly segments: (session: NodeAgentSession, input: {
|
|
17
|
+
readonly cursor?: string;
|
|
18
|
+
readonly limit?: number;
|
|
19
|
+
}) => Promise<unknown>;
|
|
16
20
|
readonly context: SessionContextService;
|
|
17
21
|
readonly discover: (runner: RunnerName, workspaceId?: string) => Promise<readonly NodeAgentSession[]>;
|
|
18
22
|
readonly workspaceIdForCwd: (cwd: string) => string | undefined;
|
|
@@ -23,7 +23,12 @@ export class SessionQueryService {
|
|
|
23
23
|
}
|
|
24
24
|
async turns(input) {
|
|
25
25
|
const page = pageInput(input);
|
|
26
|
-
|
|
26
|
+
const session = await this.requireSession(input.sessionId);
|
|
27
|
+
if (input.unit === 'SEGMENT')
|
|
28
|
+
return this.options.segments(session, page);
|
|
29
|
+
if (input.unit !== undefined && input.unit !== 'TURN')
|
|
30
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
31
|
+
return this.options.history(session, page);
|
|
27
32
|
}
|
|
28
33
|
events(input) {
|
|
29
34
|
const page = pageInput(input);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { lstat, open,
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
1
|
+
import { lstat, open, readdir, readlink, realpath } from 'node:fs/promises';
|
|
4
2
|
import path from 'node:path';
|
|
5
3
|
import { skillInstallMetadataSchema } from '@myagentroam/protocol';
|
|
4
|
+
import { hasBrokenWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
|
|
6
5
|
const MAX_METADATA_BYTES = 64 * 1024;
|
|
7
6
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
8
7
|
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
|
|
@@ -21,7 +20,7 @@ export class SkillDirectoryService {
|
|
|
21
20
|
const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
|
|
22
21
|
const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
|
|
23
22
|
const skills = mergeWorkspaceCopies(agents, claude);
|
|
24
|
-
const gitExcludeBroken = await
|
|
23
|
+
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name));
|
|
25
24
|
return {
|
|
26
25
|
targetKind: 'WORKSPACE',
|
|
27
26
|
compatibilityMode: 'MANAGED_COPY',
|
|
@@ -31,28 +30,6 @@ export class SkillDirectoryService {
|
|
|
31
30
|
};
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
async function hasBrokenGitExclude(workspace, names) {
|
|
35
|
-
if (names.length === 0)
|
|
36
|
-
return false;
|
|
37
|
-
try {
|
|
38
|
-
await lstat(path.join(workspace, '.git'));
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
if (isMissing(error))
|
|
42
|
-
return false;
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
const result = await promisify(execFile)('git', ['-C', workspace, 'rev-parse', '--git-path', 'info/exclude'], { timeout: 5_000, windowsHide: true });
|
|
47
|
-
const exclude = path.resolve(workspace, result.stdout.trim());
|
|
48
|
-
const content = await readFile(exclude, 'utf8');
|
|
49
|
-
return names.some((name) => !content.includes(`/.agents/skills/${name}/`) ||
|
|
50
|
-
!content.includes(`/.claude/skills/${name}/`));
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
33
|
async function scanRoot(root, source) {
|
|
57
34
|
let entries;
|
|
58
35
|
try {
|
|
@@ -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 { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
|
|
7
8
|
export class SkillInstallService {
|
|
8
9
|
async recoverNodeHome(home) {
|
|
9
10
|
const agentsRoot = path.join(home, '.agents', 'skills');
|
|
@@ -58,7 +59,7 @@ export class SkillInstallService {
|
|
|
58
59
|
await mkdir(claudeRoot, { recursive: true });
|
|
59
60
|
await assertContainedDirectory(workspace, agentsRoot);
|
|
60
61
|
await assertContainedDirectory(workspace, claudeRoot);
|
|
61
|
-
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () =>
|
|
62
|
+
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true), () => updateWorkspaceSkillGitExclude(workspace, name, false));
|
|
62
63
|
}
|
|
63
64
|
async removeWorkspace(workspace, name) {
|
|
64
65
|
assertName(name);
|
|
@@ -66,7 +67,7 @@ export class SkillInstallService {
|
|
|
66
67
|
const claudeRoot = path.join(workspace, '.claude', 'skills');
|
|
67
68
|
await assertExistingContainedDirectory(workspace, agentsRoot);
|
|
68
69
|
await assertExistingContainedDirectory(workspace, claudeRoot);
|
|
69
|
-
await removePair(agentsRoot, claudeRoot, name, () =>
|
|
70
|
+
await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false), () => updateWorkspaceSkillGitExclude(workspace, name, true));
|
|
70
71
|
}
|
|
71
72
|
async removeNodeHome(home, name) {
|
|
72
73
|
assertName(name);
|
|
@@ -100,7 +101,7 @@ export class SkillInstallService {
|
|
|
100
101
|
if (!isMissing(error))
|
|
101
102
|
throw error;
|
|
102
103
|
}
|
|
103
|
-
await
|
|
104
|
+
await rewriteWorkspaceGitExclude(workspace, names);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
export async function workspaceSkillTracked(workspace, name) {
|
|
@@ -405,74 +406,6 @@ async function ensureClaudeRoot(agentsRoot, claudeRoot) {
|
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
408
|
}
|
|
408
|
-
async function updateGitExclude(workspace, name, add) {
|
|
409
|
-
await mutateGitExclude(workspace, (entries) => {
|
|
410
|
-
for (const root of ['.agents', '.claude']) {
|
|
411
|
-
const line = `/${root}/skills/${name}/`;
|
|
412
|
-
if (add)
|
|
413
|
-
entries.add(line);
|
|
414
|
-
else
|
|
415
|
-
entries.delete(line);
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
async function rewriteGitExclude(workspace, names) {
|
|
420
|
-
await mutateGitExclude(workspace, (entries) => {
|
|
421
|
-
entries.clear();
|
|
422
|
-
for (const name of names)
|
|
423
|
-
for (const root of ['.agents', '.claude'])
|
|
424
|
-
entries.add(`/${root}/skills/${name}/`);
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
async function mutateGitExclude(workspace, mutate) {
|
|
428
|
-
const git = path.join(workspace, '.git');
|
|
429
|
-
if (!(await exists(git)))
|
|
430
|
-
return;
|
|
431
|
-
let exclude;
|
|
432
|
-
try {
|
|
433
|
-
const result = await promisify(execFile)('git', ['-C', workspace, 'rev-parse', '--git-path', 'info/exclude', '--git-common-dir'], { timeout: 5_000, windowsHide: true });
|
|
434
|
-
const [excludeOutput, commonOutput] = result.stdout.trim().split(/\r?\n/u);
|
|
435
|
-
if (!excludeOutput || !commonOutput)
|
|
436
|
-
throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
|
|
437
|
-
exclude = path.resolve(workspace, excludeOutput);
|
|
438
|
-
const common = path.resolve(workspace, commonOutput);
|
|
439
|
-
const relative = path.relative(common, exclude);
|
|
440
|
-
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
441
|
-
throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
|
|
445
|
-
}
|
|
446
|
-
await mkdir(path.dirname(exclude), { recursive: true });
|
|
447
|
-
const begin = '# BEGIN MyAgentRoam Skills';
|
|
448
|
-
const end = '# END MyAgentRoam Skills';
|
|
449
|
-
let content = '';
|
|
450
|
-
try {
|
|
451
|
-
const excludeStat = await lstat(exclude);
|
|
452
|
-
if (!excludeStat.isFile() || excludeStat.isSymbolicLink())
|
|
453
|
-
throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
|
|
454
|
-
content = await readFile(exclude, 'utf8');
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
if (!isMissing(error))
|
|
458
|
-
throw error;
|
|
459
|
-
}
|
|
460
|
-
const pattern = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, 'u');
|
|
461
|
-
const existing = pattern.exec(content)?.[0] ?? '';
|
|
462
|
-
const entries = new Set(existing.split(/\r?\n/u).filter((line) => line.startsWith('/.')));
|
|
463
|
-
mutate(entries);
|
|
464
|
-
const block = entries.size === 0 ? '' : `${begin}\n${[...entries].sort().join('\n')}\n${end}\n`;
|
|
465
|
-
const next = content.replace(pattern, '').replace(/\s*$/u, '\n') + block;
|
|
466
|
-
const staging = path.join(path.dirname(exclude), `.mar-git-exclude-${randomUUID()}.tmp`);
|
|
467
|
-
try {
|
|
468
|
-
await writeFile(staging, next, { encoding: 'utf8', flag: 'wx' });
|
|
469
|
-
await rename(staging, exclude);
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
await rm(staging, { force: true });
|
|
473
|
-
throw error;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
409
|
function assertName(name) {
|
|
477
410
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) ||
|
|
478
411
|
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u.test(name))
|
|
@@ -499,6 +432,3 @@ async function exists(value) {
|
|
|
499
432
|
function isMissing(error) {
|
|
500
433
|
return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
|
|
501
434
|
}
|
|
502
|
-
function escapeRegExp(value) {
|
|
503
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
504
|
-
}
|
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { skillInstallMetadataSchema } from '@myagentroam/protocol';
|
|
4
4
|
import { SkillDirectoryService } from './skill-directory-service.js';
|
|
5
5
|
import { SkillInstallService, workspaceSkillTracked } from './skill-install-service.js';
|
|
6
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
6
7
|
export class SkillNodeOperationService {
|
|
7
8
|
database;
|
|
8
9
|
config;
|
|
@@ -76,7 +77,7 @@ export class SkillNodeOperationService {
|
|
|
76
77
|
catch (error) {
|
|
77
78
|
terminal = {
|
|
78
79
|
status: 'FAILED',
|
|
79
|
-
errorCode: error
|
|
80
|
+
errorCode: safeErrorCode(error, 'SKILL_INSTALL_FAILED')
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
this.activeTargets.delete(targetKey);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function ensureWorkspaceGitExclude(workspace: string): Promise<void>;
|
|
2
|
+
export declare function updateWorkspaceSkillGitExclude(workspace: string, name: string, add: boolean): Promise<void>;
|
|
3
|
+
export declare function rewriteWorkspaceGitExclude(workspace: string, names: ReadonlySet<string>): Promise<void>;
|
|
4
|
+
export declare function hasBrokenWorkspaceSkillGitExclude(workspace: string, names: readonly string[]): Promise<boolean>;
|