@myagentroam/node 0.9.0 → 0.9.3
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 +50 -25
- package/dist/control-outbound-scheduler.d.ts +4 -1
- package/dist/control-outbound-scheduler.js +39 -6
- package/dist/database.d.ts +0 -19
- package/dist/database.js +1 -62
- package/dist/event-buffer.d.ts +2 -5
- package/dist/event-buffer.js +3 -12
- package/dist/native-session-history.js +105 -26
- package/dist/runner/abstract-runner.d.ts +1 -1
- package/dist/runner/abstract-runner.js +2 -2
- package/dist/runner/claude/managed-run-controller.d.ts +0 -1
- package/dist/runner/claude/managed-run-controller.js +32 -17
- package/dist/runner/claude-code-runner.js +3 -4
- package/dist/runner/codex/conversation-parser.d.ts +3 -3
- package/dist/runner/codex/conversation-parser.js +33 -12
- package/dist/runner/codex/managed-run-controller.d.ts +2 -2
- package/dist/runner/codex/managed-run-controller.js +26 -10
- package/dist/runner/codex-runner.d.ts +3 -1
- package/dist/runner/codex-runner.js +40 -10
- package/dist/runner/opencode/conversation-parser.d.ts +11 -7
- package/dist/runner/opencode/conversation-parser.js +16 -16
- package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
- package/dist/runner/opencode/managed-run-controller.js +94 -22
- package/dist/runner/opencode-runner.d.ts +3 -0
- package/dist/runner/opencode-runner.js +25 -0
- package/dist/runner-command-engine.js +1 -1
- package/dist/runner-profiles.js +29 -17
- package/dist/runner-usage.js +5 -5
- package/dist/runtime-state.d.ts +5 -12
- package/dist/runtime-state.js +32 -46
- package/dist/service/attachment-domain-state.d.ts +2 -0
- package/dist/service/attachment-domain-state.js +2 -0
- 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 +156 -0
- package/dist/service/external-session-resume-service.js +9 -0
- package/dist/service/fake-managed-run-service.d.ts +0 -1
- package/dist/service/fake-managed-run-service.js +0 -2
- package/dist/service/mcp-installation-verifier.js +1 -1
- package/dist/service/native-session-projection-service.js +5 -0
- 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-connection-lifecycle-service.d.ts +0 -2
- package/dist/service/node-connection-lifecycle-service.js +0 -1
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-control-message-service.d.ts +0 -1
- package/dist/service/node-control-message-service.js +9 -6
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/node-workspace-coordinator.d.ts +1 -4
- package/dist/service/node-workspace-coordinator.js +2 -12
- package/dist/service/run-attachment-service.d.ts +3 -2
- package/dist/service/run-attachment-service.js +51 -17
- package/dist/service/run-event-service.d.ts +2 -0
- package/dist/service/run-event-service.js +77 -2
- package/dist/service/run-workbench-service.d.ts +1 -1
- package/dist/service/run-workbench-service.js +1 -5
- package/dist/service/runner-interaction-service.d.ts +1 -2
- package/dist/service/runner-interaction-service.js +1 -4
- package/dist/service/session-catalog-service.js +4 -4
- package/dist/service/session-context-service.d.ts +1 -0
- package/dist/service/session-context-service.js +14 -0
- package/dist/service/session-domain-state.d.ts +1 -0
- package/dist/service/session-domain-state.js +1 -0
- package/dist/service/session-message-service.js +5 -10
- 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.d.ts +3 -1
- package/dist/service/skill-directory-service.js +31 -30
- package/dist/service/skill-install-service.js +4 -74
- package/dist/service/skill-node-operation-service.d.ts +1 -1
- package/dist/service/skill-node-operation-service.js +6 -3
- package/dist/service/workbench-event-service.d.ts +1 -13
- package/dist/service/workbench-event-service.js +2 -36
- package/dist/service/workbench-manifest-service.d.ts +0 -1
- package/dist/service/workbench-manifest-service.js +0 -1
- package/dist/service/workspace-domain-state.d.ts +1 -0
- 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.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.js +4 -2
- package/dist/service/workspace-service.js +2 -0
- package/dist/service/workspace-upload-service.d.ts +2 -0
- package/dist/service/workspace-upload-service.js +28 -6
- package/dist/service/workspace-workbench-service.d.ts +0 -3
- package/dist/service/workspace-workbench-service.js +0 -17
- 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-error.d.ts +6 -0
- package/dist/util/runner-error.js +52 -0
- package/dist/util/runner-native-session-parsers.d.ts +2 -3
- package/dist/util/runner-native-session-parsers.js +48 -20
- package/dist/util/safe-error.d.ts +2 -0
- package/dist/util/safe-error.js +5 -0
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -47,6 +50,9 @@ export class RunEventService {
|
|
|
47
50
|
this.emitNow(runId, 'text.delta', { text: pending.text });
|
|
48
51
|
}
|
|
49
52
|
emitNow(runId, eventType, payload, status) {
|
|
53
|
+
if (this.options.runtime.getRun(runId) === undefined)
|
|
54
|
+
return;
|
|
55
|
+
const notificationPayload = this.notificationPayload(runId, eventType, payload, status);
|
|
50
56
|
const event = {
|
|
51
57
|
runId,
|
|
52
58
|
sequence: (this.sequences.get(runId) ?? 0) + 1,
|
|
@@ -59,8 +65,9 @@ export class RunEventService {
|
|
|
59
65
|
nodeLog('run.event', { runId, eventType, status });
|
|
60
66
|
if (status === 'FAILED')
|
|
61
67
|
this.options.metrics.runnerFailed();
|
|
68
|
+
let persisted = undefined;
|
|
62
69
|
try {
|
|
63
|
-
this.options.runtime.appendRunEvent({
|
|
70
|
+
persisted = this.options.runtime.appendRunEvent({
|
|
64
71
|
runId,
|
|
65
72
|
sequence: event.sequence,
|
|
66
73
|
eventType,
|
|
@@ -72,6 +79,7 @@ export class RunEventService {
|
|
|
72
79
|
/* A stale relayed run may race the storage boundary. */
|
|
73
80
|
}
|
|
74
81
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
82
|
+
this.options.state.active.delete(runId);
|
|
75
83
|
this.options.state.interruptRequested.delete(runId);
|
|
76
84
|
const terminalRun = this.options.runtime.getRun(runId);
|
|
77
85
|
if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
|
|
@@ -84,7 +92,14 @@ export class RunEventService {
|
|
|
84
92
|
if (session !== undefined)
|
|
85
93
|
this.options.emitWorkbench('session', { session: this.options.presentSession(session) });
|
|
86
94
|
}
|
|
87
|
-
this.options.emitWorkbench('conversation', {
|
|
95
|
+
this.options.emitWorkbench('conversation', {
|
|
96
|
+
event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
|
|
97
|
+
});
|
|
98
|
+
if (notificationPayload !== undefined)
|
|
99
|
+
this.emitNow(runId, 'notification', {
|
|
100
|
+
...notificationPayload,
|
|
101
|
+
occurredAt: persisted?.createdAt ?? Date.now()
|
|
102
|
+
});
|
|
88
103
|
if (status === undefined || !isTerminalRunStatus(status))
|
|
89
104
|
return;
|
|
90
105
|
const turn = this.options.runtime.conversationTurnForRun(runId);
|
|
@@ -96,7 +111,67 @@ export class RunEventService {
|
|
|
96
111
|
const run = this.options.runtime.getRun(runId);
|
|
97
112
|
if (run !== undefined) {
|
|
98
113
|
this.options.scheduleContextRefresh(run.sessionId);
|
|
114
|
+
this.options.runtime.removeTerminalRun(runId);
|
|
115
|
+
this.sequences.delete(runId);
|
|
99
116
|
void this.options.dispatchQueue(run.sessionId);
|
|
100
117
|
}
|
|
101
118
|
}
|
|
119
|
+
notificationPayload(runId, eventType, payload, status) {
|
|
120
|
+
if (eventType === 'notification')
|
|
121
|
+
return undefined;
|
|
122
|
+
const run = this.options.runtime.getRun(runId);
|
|
123
|
+
if (run === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const session = this.options.runtime.getAgentSession(run.sessionId);
|
|
126
|
+
if (session === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const base = {
|
|
129
|
+
workspaceId: run.workspaceId,
|
|
130
|
+
sessionId: run.sessionId,
|
|
131
|
+
sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
|
|
132
|
+
runId
|
|
133
|
+
};
|
|
134
|
+
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
135
|
+
if (this.options.state.interruptRequested.has(runId))
|
|
136
|
+
return undefined;
|
|
137
|
+
if (status === 'SUCCEEDED' &&
|
|
138
|
+
this.options.runtime.sessionQueue(run.sessionId).runIds.length > 0)
|
|
139
|
+
return undefined;
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
notificationId: `run:${run.workspaceId}:${runId}`,
|
|
143
|
+
kind: 'RUN_TERMINAL',
|
|
144
|
+
outcome: status
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (eventType === 'approval.request' && isPlainRecord(payload)) {
|
|
148
|
+
const approvalId = payload.approvalId;
|
|
149
|
+
if (typeof approvalId !== 'string')
|
|
150
|
+
return undefined;
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
notificationId: `approval:${run.workspaceId}:${runId}:${approvalId}`,
|
|
154
|
+
kind: 'APPROVAL_REQUIRED'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (eventType !== 'conversation.item' || !isPlainRecord(payload))
|
|
158
|
+
return undefined;
|
|
159
|
+
if (payload.kind === 'plan' && typeof payload.itemId === 'string')
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
notificationId: `plan:${run.workspaceId}:${run.sessionId}`,
|
|
163
|
+
kind: 'PLAN_READY'
|
|
164
|
+
};
|
|
165
|
+
if (payload.kind === 'user_input_request' &&
|
|
166
|
+
typeof payload.itemId === 'string' &&
|
|
167
|
+
isPlainRecord(payload.payload) &&
|
|
168
|
+
Array.isArray(payload.payload.questions) &&
|
|
169
|
+
payload.payload.questions.length > 0)
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
notificationId: `user-input:${run.workspaceId}:${runId}:${payload.itemId}`,
|
|
173
|
+
kind: 'USER_INPUT_REQUIRED'
|
|
174
|
+
};
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
102
177
|
}
|
|
@@ -6,6 +6,7 @@ export interface RunWorkbenchServiceOptions {
|
|
|
6
6
|
readonly respondUserInput: (runId: string, requestId: string, answers: Record<string, unknown>) => void;
|
|
7
7
|
readonly deleteQueuedStart: (runId: string) => void;
|
|
8
8
|
readonly clearImages: (runId: string) => void;
|
|
9
|
+
readonly cleanupAttachments: (runId: string) => void;
|
|
9
10
|
readonly emitQueue: (workspaceId: string, sessionId: string) => void;
|
|
10
11
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'CANCELLED') => void;
|
|
11
12
|
readonly control: (operation: string, payload: Record<string, unknown>) => void;
|
|
@@ -16,7 +17,6 @@ export declare class RunWorkbenchService {
|
|
|
16
17
|
constructor(options: RunWorkbenchServiceOptions);
|
|
17
18
|
operations(): Readonly<Record<string, NodeOperationHandler>>;
|
|
18
19
|
private get;
|
|
19
|
-
private snapshot;
|
|
20
20
|
private image;
|
|
21
21
|
private respond;
|
|
22
22
|
private interruptSession;
|
|
@@ -8,7 +8,6 @@ export class RunWorkbenchService {
|
|
|
8
8
|
operations() {
|
|
9
9
|
return {
|
|
10
10
|
'run.get': (data) => this.get(record(data)),
|
|
11
|
-
'run.snapshot': (data) => this.snapshot(record(data)),
|
|
12
11
|
'run.image.read': (data) => this.image(record(data)),
|
|
13
12
|
'run.user-input.respond': (data) => this.respond(record(data)),
|
|
14
13
|
'session.interrupt': (data) => this.interruptSession(record(data)),
|
|
@@ -21,10 +20,6 @@ export class RunWorkbenchService {
|
|
|
21
20
|
get(input) {
|
|
22
21
|
return { run: this.requireRun(input.runId) };
|
|
23
22
|
}
|
|
24
|
-
snapshot(input) {
|
|
25
|
-
const run = this.requireRun(input.runId);
|
|
26
|
-
return { snapshot: this.options.runtime.getSnapshot(run.id) ?? null };
|
|
27
|
-
}
|
|
28
23
|
async image(input) {
|
|
29
24
|
if (typeof input.runId !== 'string' ||
|
|
30
25
|
typeof input.imageIndex !== 'number' ||
|
|
@@ -63,6 +58,7 @@ export class RunWorkbenchService {
|
|
|
63
58
|
this.options.runtime.deleteQueuedRun(run.id);
|
|
64
59
|
this.options.deleteQueuedStart(run.id);
|
|
65
60
|
this.options.clearImages(run.id);
|
|
61
|
+
this.options.cleanupAttachments(run.id);
|
|
66
62
|
this.options.emitQueue(run.workspaceId, run.sessionId);
|
|
67
63
|
return { cancelled: true };
|
|
68
64
|
}
|
|
@@ -13,7 +13,6 @@ interface RunnerInteractionServiceOptions {
|
|
|
13
13
|
readonly runners: RunnerRegistry;
|
|
14
14
|
readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED' | 'INTERRUPTED') => void;
|
|
15
15
|
readonly emitItem: (runId: string, item: PublicConversationItem) => void;
|
|
16
|
-
readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
|
|
17
16
|
readonly approvalTimeoutMs: number;
|
|
18
17
|
readonly publishApproval: (input: {
|
|
19
18
|
readonly runId: string;
|
|
@@ -33,7 +32,7 @@ export declare class RunnerInteractionService {
|
|
|
33
32
|
cancelClaudeRun(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
|
|
34
33
|
cancelCodexTurn(runId: string, threadId: string, turnId: string, cwd: string, terminalStatus: 'CANCELLED' | 'INTERRUPTED'): Promise<void>;
|
|
35
34
|
respondUserInput(runId: string, requestId: string, rawAnswers: Record<string, unknown>): void;
|
|
36
|
-
completeCancelledRun(runId: string
|
|
35
|
+
completeCancelledRun(runId: string): boolean;
|
|
37
36
|
private respondCodexUserInput;
|
|
38
37
|
private answers;
|
|
39
38
|
}
|
|
@@ -162,7 +162,6 @@ export class RunnerInteractionService {
|
|
|
162
162
|
codex.execution.runs.delete(runId);
|
|
163
163
|
codex.releaseEnvironment(runId);
|
|
164
164
|
this.options.state.active.delete(runId);
|
|
165
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
respondUserInput(runId, requestId, rawAnswers) {
|
|
@@ -194,7 +193,7 @@ export class RunnerInteractionService {
|
|
|
194
193
|
merge: true
|
|
195
194
|
});
|
|
196
195
|
}
|
|
197
|
-
completeCancelledRun(runId
|
|
196
|
+
completeCancelledRun(runId) {
|
|
198
197
|
const run = this.options.runtime.getRun(runId);
|
|
199
198
|
if (run === undefined)
|
|
200
199
|
return false;
|
|
@@ -206,8 +205,6 @@ export class RunnerInteractionService {
|
|
|
206
205
|
return false;
|
|
207
206
|
this.options.codex().execution.runs.delete(runId);
|
|
208
207
|
this.options.state.active.delete(runId);
|
|
209
|
-
if (cwd !== undefined)
|
|
210
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
211
208
|
return true;
|
|
212
209
|
}
|
|
213
210
|
respondCodexUserInput(runId, requestId, rawAnswers) {
|
|
@@ -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);
|
|
@@ -24,6 +24,7 @@ export interface SessionContextServiceOptions {
|
|
|
24
24
|
export declare class SessionContextService {
|
|
25
25
|
private readonly options;
|
|
26
26
|
private readonly snapshots;
|
|
27
|
+
private readonly snapshotTimers;
|
|
27
28
|
private readonly timers;
|
|
28
29
|
private readonly versions;
|
|
29
30
|
constructor(options: SessionContextServiceOptions);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const REFRESH_DELAYS_MS = [100, 400, 1_000];
|
|
2
|
+
const CONTEXT_SNAPSHOT_TTL_MS = 30 * 60_000;
|
|
2
3
|
export class SessionContextService {
|
|
3
4
|
options;
|
|
4
5
|
snapshots = new Map();
|
|
6
|
+
snapshotTimers = new Map();
|
|
5
7
|
timers = new Map();
|
|
6
8
|
versions = new Map();
|
|
7
9
|
constructor(options) {
|
|
@@ -20,6 +22,15 @@ export class SessionContextService {
|
|
|
20
22
|
percentage: Math.round((usage.usedTokens / usage.maxTokens) * 1000) / 10
|
|
21
23
|
};
|
|
22
24
|
this.snapshots.set(session.id, context);
|
|
25
|
+
const previousTimer = this.snapshotTimers.get(session.id);
|
|
26
|
+
if (previousTimer !== undefined)
|
|
27
|
+
clearTimeout(previousTimer);
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
this.snapshotTimers.delete(session.id);
|
|
30
|
+
this.snapshots.delete(session.id);
|
|
31
|
+
}, CONTEXT_SNAPSHOT_TTL_MS);
|
|
32
|
+
timer.unref();
|
|
33
|
+
this.snapshotTimers.set(session.id, timer);
|
|
23
34
|
return { context, pending: false };
|
|
24
35
|
}
|
|
25
36
|
schedule(sessionId) {
|
|
@@ -71,7 +82,10 @@ export class SessionContextService {
|
|
|
71
82
|
dispose() {
|
|
72
83
|
for (const timer of this.timers.values())
|
|
73
84
|
clearTimeout(timer);
|
|
85
|
+
for (const timer of this.snapshotTimers.values())
|
|
86
|
+
clearTimeout(timer);
|
|
74
87
|
this.timers.clear();
|
|
88
|
+
this.snapshotTimers.clear();
|
|
75
89
|
this.versions.clear();
|
|
76
90
|
this.snapshots.clear();
|
|
77
91
|
}
|
|
@@ -5,4 +5,5 @@ export declare class SessionDomainState<TSession, TActivity> {
|
|
|
5
5
|
readonly resumeFailures: Map<string, string>;
|
|
6
6
|
readonly messageResumeFailures: Map<string, string>;
|
|
7
7
|
readonly resumeFailureDetails: Map<string, string>;
|
|
8
|
+
readonly failureTimers: Map<string, NodeJS.Timeout>;
|
|
8
9
|
}
|
|
@@ -68,11 +68,13 @@ export class SessionMessageService {
|
|
|
68
68
|
this.options.attachments.cache(created.run.id, imageAttachments);
|
|
69
69
|
let attachmentPaths;
|
|
70
70
|
try {
|
|
71
|
-
attachmentPaths = await this.persistAttachments(session, created.run.id,
|
|
71
|
+
attachmentPaths = await this.persistAttachments(session, created.run.id, attachments);
|
|
72
|
+
await this.options.uploads.consume(attachmentIds);
|
|
72
73
|
}
|
|
73
74
|
catch (error) {
|
|
74
75
|
this.options.runtime.deleteQueuedRun(created.run.id);
|
|
75
76
|
this.options.attachments.clear(created.run.id);
|
|
77
|
+
this.options.attachments.cleanup(created.run.id);
|
|
76
78
|
throw error;
|
|
77
79
|
}
|
|
78
80
|
const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
|
|
@@ -117,15 +119,8 @@ export class SessionMessageService {
|
|
|
117
119
|
idempotent: false
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
|
-
async persistAttachments(session, runId,
|
|
121
|
-
return
|
|
122
|
-
...uploaded
|
|
123
|
-
.filter(({ attachment }) => !isRunnerImageAttachment(attachment))
|
|
124
|
-
.map(({ path }) => path),
|
|
125
|
-
...(await this.options.attachments.persistFiles(session.cwd, runId, attachmentIds.length === 0
|
|
126
|
-
? attachments.filter((attachment) => !isRunnerImageAttachment(attachment))
|
|
127
|
-
: []))
|
|
128
|
-
];
|
|
122
|
+
async persistAttachments(session, runId, attachments) {
|
|
123
|
+
return this.options.attachments.persistFiles(session.cwd, runId, attachments.filter((attachment) => !isRunnerImageAttachment(attachment)));
|
|
129
124
|
}
|
|
130
125
|
channelMessage(input) {
|
|
131
126
|
if (typeof input.sessionId !== 'string' ||
|
|
@@ -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);
|
|
@@ -12,6 +12,8 @@ export interface LocalSkillTargetInspection {
|
|
|
12
12
|
readonly skills: readonly LocalSkillInspection[];
|
|
13
13
|
}
|
|
14
14
|
export declare class SkillDirectoryService {
|
|
15
|
-
inspectNodeHome(home: string
|
|
15
|
+
inspectNodeHome(home: string, options?: {
|
|
16
|
+
readonly includeClaudeCompatibility?: boolean;
|
|
17
|
+
}): Promise<LocalSkillTargetInspection>;
|
|
16
18
|
inspectWorkspace(workspace: string): Promise<LocalSkillTargetInspection>;
|
|
17
19
|
}
|
|
@@ -1,27 +1,32 @@
|
|
|
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;
|
|
9
8
|
export class SkillDirectoryService {
|
|
10
|
-
async inspectNodeHome(home) {
|
|
9
|
+
async inspectNodeHome(home, options = {}) {
|
|
11
10
|
const agentsRoot = path.join(home, '.agents', 'skills');
|
|
12
11
|
const claudeRoot = path.join(home, '.claude', 'skills');
|
|
13
|
-
const
|
|
12
|
+
const compatibilityMode = await nodeCompatibilityMode(agentsRoot, claudeRoot);
|
|
13
|
+
const agents = await scanRoot(agentsRoot, 'AGENTS');
|
|
14
|
+
const claude = options.includeClaudeCompatibility === true && compatibilityMode !== 'UNAVAILABLE'
|
|
15
|
+
? await scanRoot(claudeRoot, 'CLAUDE')
|
|
16
|
+
: [];
|
|
14
17
|
return {
|
|
15
18
|
targetKind: 'NODE',
|
|
16
|
-
compatibilityMode
|
|
17
|
-
skills
|
|
19
|
+
compatibilityMode,
|
|
20
|
+
skills: options.includeClaudeCompatibility === true
|
|
21
|
+
? mergeNodeCompatibilityRoots(agents, claude)
|
|
22
|
+
: agents.map(withoutSource)
|
|
18
23
|
};
|
|
19
24
|
}
|
|
20
25
|
async inspectWorkspace(workspace) {
|
|
21
26
|
const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
|
|
22
27
|
const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
|
|
23
28
|
const skills = mergeWorkspaceCopies(agents, claude);
|
|
24
|
-
const gitExcludeBroken = await
|
|
29
|
+
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name));
|
|
25
30
|
return {
|
|
26
31
|
targetKind: 'WORKSPACE',
|
|
27
32
|
compatibilityMode: 'MANAGED_COPY',
|
|
@@ -31,28 +36,6 @@ export class SkillDirectoryService {
|
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
}
|
|
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
39
|
async function scanRoot(root, source) {
|
|
57
40
|
let entries;
|
|
58
41
|
try {
|
|
@@ -170,6 +153,24 @@ function mergeWorkspaceCopies(agents, claude) {
|
|
|
170
153
|
return withoutSource(copies.agents);
|
|
171
154
|
});
|
|
172
155
|
}
|
|
156
|
+
function mergeNodeCompatibilityRoots(agents, claude) {
|
|
157
|
+
const byName = new Map();
|
|
158
|
+
for (const item of agents)
|
|
159
|
+
byName.set(item.name, { agents: item });
|
|
160
|
+
for (const item of claude)
|
|
161
|
+
byName.set(item.name, { ...byName.get(item.name), claude: item });
|
|
162
|
+
return [...byName.entries()]
|
|
163
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
164
|
+
.map(([name, copies]) => {
|
|
165
|
+
if (copies.agents === undefined || copies.claude === undefined)
|
|
166
|
+
return withoutSource((copies.agents ?? copies.claude));
|
|
167
|
+
if (copies.agents.description !== copies.claude.description ||
|
|
168
|
+
copies.agents.localStatus !== copies.claude.localStatus ||
|
|
169
|
+
JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
|
|
170
|
+
return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
|
|
171
|
+
return withoutSource(copies.agents);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
173
174
|
function withoutSource(entry) {
|
|
174
175
|
return {
|
|
175
176
|
name: entry.name,
|
|
@@ -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
|
-
}
|
|
@@ -9,7 +9,7 @@ export declare class SkillNodeOperationService {
|
|
|
9
9
|
private readonly jobs;
|
|
10
10
|
constructor(database: () => NodeDatabase, config: () => NodeConfig);
|
|
11
11
|
operations(): {
|
|
12
|
-
'node.skills.inspect': () => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
|
|
12
|
+
'node.skills.inspect': (data: unknown) => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
|
|
13
13
|
'node.skills.install': (data: unknown) => Promise<{
|
|
14
14
|
accepted: boolean;
|
|
15
15
|
installJobId: `${string}-${string}-${string}-${string}-${string}`;
|
|
@@ -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;
|
|
@@ -16,9 +17,11 @@ export class SkillNodeOperationService {
|
|
|
16
17
|
}
|
|
17
18
|
operations() {
|
|
18
19
|
return {
|
|
19
|
-
'node.skills.inspect': async () => {
|
|
20
|
+
'node.skills.inspect': async (data) => {
|
|
20
21
|
await this.installer.recoverNodeHome(homedir());
|
|
21
|
-
return this.directories.inspectNodeHome(homedir()
|
|
22
|
+
return this.directories.inspectNodeHome(homedir(), {
|
|
23
|
+
includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] === true
|
|
24
|
+
});
|
|
22
25
|
},
|
|
23
26
|
'node.skills.install': (data) => this.install('NODE', data),
|
|
24
27
|
'node.skills.remove': (data) => this.remove('NODE', data),
|
|
@@ -76,7 +79,7 @@ export class SkillNodeOperationService {
|
|
|
76
79
|
catch (error) {
|
|
77
80
|
terminal = {
|
|
78
81
|
status: 'FAILED',
|
|
79
|
-
errorCode: error
|
|
82
|
+
errorCode: safeErrorCode(error, 'SKILL_INSTALL_FAILED')
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
this.activeTargets.delete(targetKey);
|