@myagentroam/marctl 0.11.2 → 0.11.4
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/package.json +1 -1
- package/src/cli.mjs +54 -29
- package/src/client.mjs +1 -1
- package/src/config.mjs +1 -11
- package/src/direct-control.mjs +225 -0
- package/src/direct-errors.mjs +48 -0
- package/src/direct-process.mjs +40 -17
- package/src/direct-transport.mjs +235 -0
- package/src/direct.mjs +102 -494
- package/src/errors.mjs +11 -0
- package/src/i18n/en-US.mjs +6 -3
- package/src/i18n/zh-CN.mjs +5 -3
- package/src/scp-channel.mjs +99 -0
- package/src/scp-copy.mjs +46 -0
- package/src/scp-endpoint.mjs +29 -0
- package/src/scp-protocol.mjs +2 -0
- package/src/scp-workers.mjs +179 -0
- package/src/scp.mjs +26 -344
- package/src/session-args.mjs +176 -0
- package/src/session-input.mjs +88 -0
- package/src/session-prepare.mjs +51 -0
- package/src/session-target.mjs +25 -0
- package/src/session-wait.mjs +232 -0
- package/src/sessions.mjs +27 -567
- package/src/terminal-frame.mjs +48 -0
- package/src/terminal-interaction.mjs +177 -0
- package/src/terminal-race.mjs +117 -0
- package/src/terminal-relay.mjs +305 -0
- package/src/terminal.mjs +50 -603
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { messageContent, imageAttachments } from './session-input.mjs';
|
|
3
|
+
|
|
4
|
+
export async function prepareSessionRequest({ args, env, stdin, translate, sessionId, targeted }) {
|
|
5
|
+
const body = { action: args.action };
|
|
6
|
+
for (const name of [
|
|
7
|
+
'sessionId',
|
|
8
|
+
'parentSessionId',
|
|
9
|
+
'runner',
|
|
10
|
+
'model',
|
|
11
|
+
'effort',
|
|
12
|
+
'title',
|
|
13
|
+
'cursor',
|
|
14
|
+
'limit',
|
|
15
|
+
'query',
|
|
16
|
+
'runId',
|
|
17
|
+
'approvalId',
|
|
18
|
+
'cron',
|
|
19
|
+
'expectedRevision'
|
|
20
|
+
])
|
|
21
|
+
if (args[name] !== undefined) body[name] = args[name];
|
|
22
|
+
if (targeted) body.sessionId = sessionId;
|
|
23
|
+
if (args.action === 'current' && env.MAR_CTL_SESSION_ID) body.sessionId = env.MAR_CTL_SESSION_ID;
|
|
24
|
+
if (args.action === 'start' && env.MAR_CTL_SESSION_ID)
|
|
25
|
+
body.parentSessionId = env.MAR_CTL_SESSION_ID;
|
|
26
|
+
if (['start', 'send'].includes(args.action) && env.MAR_CTL_SESSION_ID)
|
|
27
|
+
body.sourceSessionId = env.MAR_CTL_SESSION_ID;
|
|
28
|
+
if (args.messageFile !== undefined || args.message !== undefined)
|
|
29
|
+
body.content = await messageContent(
|
|
30
|
+
args,
|
|
31
|
+
stdin,
|
|
32
|
+
args.action === 'goal.set' ||
|
|
33
|
+
args.action.startsWith('schedule.') ||
|
|
34
|
+
args.action === 'prompt.set'
|
|
35
|
+
? 20000
|
|
36
|
+
: 100000,
|
|
37
|
+
translate,
|
|
38
|
+
args.action === 'prompt.set'
|
|
39
|
+
);
|
|
40
|
+
if (args.action === 'approval.decide')
|
|
41
|
+
body.decision = args.decision === 'approve' ? 'APPROVED' : 'REJECTED';
|
|
42
|
+
if (args.images !== undefined) {
|
|
43
|
+
body.attachments = await imageAttachments(args.images, translate);
|
|
44
|
+
body.content ??= '';
|
|
45
|
+
}
|
|
46
|
+
if (args.action === 'start' || args.action === 'send')
|
|
47
|
+
body.clientMessageId = args.clientMessageId ?? randomUUID();
|
|
48
|
+
if (args.action === 'send')
|
|
49
|
+
body.deliveryIntent = args.steer === true ? 'REPLACE_CURRENT' : 'QUEUE';
|
|
50
|
+
return Object.freeze(body);
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Scope is a command property, independent of input or the remote session state.
|
|
2
|
+
export const sessionTarget = Object.freeze({
|
|
3
|
+
current: 'workspace',
|
|
4
|
+
list: 'workspace',
|
|
5
|
+
start: 'workspace',
|
|
6
|
+
'schedule.list': 'workspace',
|
|
7
|
+
show: 'current',
|
|
8
|
+
config: 'current',
|
|
9
|
+
history: 'current',
|
|
10
|
+
graph: 'current',
|
|
11
|
+
rename: 'current',
|
|
12
|
+
'prompt.get': 'current',
|
|
13
|
+
'prompt.set': 'current',
|
|
14
|
+
'prompt.clear': 'current',
|
|
15
|
+
'schedule.get': 'current',
|
|
16
|
+
'schedule.create': 'current',
|
|
17
|
+
'schedule.update': 'current',
|
|
18
|
+
send: 'explicit',
|
|
19
|
+
stop: 'explicit',
|
|
20
|
+
wait: 'explicit',
|
|
21
|
+
'approval.decide': 'explicit',
|
|
22
|
+
'goal.get': 'explicit',
|
|
23
|
+
'goal.set': 'explicit',
|
|
24
|
+
'goal.clear': 'explicit'
|
|
25
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MarctlError } from './errors.mjs';
|
|
3
|
+
import { createTranslator } from './i18n/index.mjs';
|
|
4
|
+
|
|
5
|
+
const terminal = new Set(['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED']);
|
|
6
|
+
|
|
7
|
+
/** One CLI-owned Workbench connection and one existing Session Watch, never history polling. */
|
|
8
|
+
export async function waitForSessionRun({
|
|
9
|
+
client,
|
|
10
|
+
sessionId,
|
|
11
|
+
runId,
|
|
12
|
+
timeoutMs,
|
|
13
|
+
until,
|
|
14
|
+
WebSocketClass,
|
|
15
|
+
translate = createTranslator(),
|
|
16
|
+
signal
|
|
17
|
+
}) {
|
|
18
|
+
let socket;
|
|
19
|
+
const watchId = randomUUID();
|
|
20
|
+
let connectionId;
|
|
21
|
+
let renewal;
|
|
22
|
+
let settled = false;
|
|
23
|
+
let subscribed = false;
|
|
24
|
+
let progress;
|
|
25
|
+
let accept;
|
|
26
|
+
let rejectResult;
|
|
27
|
+
const result = new Promise((resolve, fail) => {
|
|
28
|
+
accept = resolve;
|
|
29
|
+
rejectResult = fail;
|
|
30
|
+
});
|
|
31
|
+
const reject = (error, stage) => {
|
|
32
|
+
if (settled) return;
|
|
33
|
+
settled = true;
|
|
34
|
+
Object.assign(error, { sessionId, runId, stage });
|
|
35
|
+
rejectResult(error);
|
|
36
|
+
};
|
|
37
|
+
const fail = (code, key, { stage, upstreamCode, ...options } = {}) => {
|
|
38
|
+
const error = new MarctlError(translate(key), { ...options, code });
|
|
39
|
+
if (upstreamCode !== undefined) error.upstreamCode = upstreamCode;
|
|
40
|
+
if (stage === 'timeout' && progress !== undefined) error.progress = progress;
|
|
41
|
+
reject(error, stage);
|
|
42
|
+
};
|
|
43
|
+
const unknown = (stage, detail, source) =>
|
|
44
|
+
fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown', {
|
|
45
|
+
stage,
|
|
46
|
+
upstreamCode:
|
|
47
|
+
typeof source?.code === 'string' && /^[A-Za-z][A-Za-z0-9_]{0,127}$/u.test(source.code)
|
|
48
|
+
? source.code
|
|
49
|
+
: undefined,
|
|
50
|
+
requestId: typeof source?.requestId === 'string' ? source.requestId.slice(0, 160) : undefined,
|
|
51
|
+
upstreamRequestId:
|
|
52
|
+
typeof source?.upstreamRequestId === 'string'
|
|
53
|
+
? source.upstreamRequestId.slice(0, 160)
|
|
54
|
+
: undefined,
|
|
55
|
+
details: [detail, source?.message, source?.details]
|
|
56
|
+
.filter((value) => typeof value === 'string')
|
|
57
|
+
.join(' ')
|
|
58
|
+
.slice(0, 4000)
|
|
59
|
+
});
|
|
60
|
+
const command = (mode) =>
|
|
61
|
+
client.sessionCommand({
|
|
62
|
+
action: 'watch',
|
|
63
|
+
sessionId,
|
|
64
|
+
workbenchConnectionId: connectionId,
|
|
65
|
+
watchId,
|
|
66
|
+
mode
|
|
67
|
+
});
|
|
68
|
+
const scheduleRenewal = (expiresAt) => {
|
|
69
|
+
clearTimeout(renewal);
|
|
70
|
+
const remaining = expiresAt - Date.now();
|
|
71
|
+
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
72
|
+
unknown('lease', 'The Watch lease is missing or expired.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
renewal = setTimeout(
|
|
76
|
+
() => {
|
|
77
|
+
command('renew')
|
|
78
|
+
.then((ack) => {
|
|
79
|
+
if (!settled) scheduleRenewal(ack?.expiresAt);
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
if ([401, 403].includes(error.statusCode)) reject(error, 'renew');
|
|
83
|
+
else
|
|
84
|
+
unknown(
|
|
85
|
+
'renew',
|
|
86
|
+
'The Watch lease could not be renewed.',
|
|
87
|
+
error instanceof MarctlError ? error : undefined
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
Math.max(50, Math.floor(remaining / 2))
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
const check = (run) => {
|
|
95
|
+
if (run?.id === runId && run.sessionId === sessionId && terminal.has(run.status) && !settled) {
|
|
96
|
+
settled = true;
|
|
97
|
+
accept({ run });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const checkApproval = () => {
|
|
101
|
+
if (settled || until !== 'approval' || progress === undefined) return;
|
|
102
|
+
if (!Array.isArray(progress.pendingApprovals)) {
|
|
103
|
+
fail('NODE_OPERATION_UNSUPPORTED', 'errors.sessionApprovalUnsupported', {
|
|
104
|
+
stage: 'approval'
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const pendingApprovals = progress.pendingApprovals.filter(
|
|
109
|
+
(approval) => approval.runId === runId
|
|
110
|
+
);
|
|
111
|
+
if (pendingApprovals.length === 0) return;
|
|
112
|
+
settled = true;
|
|
113
|
+
accept({
|
|
114
|
+
sessionId,
|
|
115
|
+
runId,
|
|
116
|
+
stopReason: 'APPROVAL_REQUIRED',
|
|
117
|
+
pendingApprovals,
|
|
118
|
+
...(progress.pendingApprovalsTruncated ? { pendingApprovalsTruncated: true } : {})
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const onMessage = (raw) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
let message;
|
|
124
|
+
try {
|
|
125
|
+
message = JSON.parse(String(raw));
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (message?.type === 'watch.event') message = message.event;
|
|
130
|
+
if (!message || typeof message !== 'object') return;
|
|
131
|
+
if (
|
|
132
|
+
message.type === 'workbench.ready' &&
|
|
133
|
+
connectionId === undefined &&
|
|
134
|
+
typeof message.workbenchConnectionId === 'string'
|
|
135
|
+
) {
|
|
136
|
+
connectionId = message.workbenchConnectionId;
|
|
137
|
+
subscribed = true;
|
|
138
|
+
command('initial').catch((error) => reject(error, 'subscribe'));
|
|
139
|
+
}
|
|
140
|
+
if (message.watchId !== watchId) return;
|
|
141
|
+
if (message.type === 'watch.failed') {
|
|
142
|
+
unknown('watch', 'The Session Watch failed.', message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (message.type === 'watch.session.snapshot') {
|
|
146
|
+
const runs = message.payload?.runs;
|
|
147
|
+
if (!Array.isArray(runs)) {
|
|
148
|
+
unknown('snapshot', 'The Watch snapshot did not contain a Run list.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const run = runs.find((value) => value?.id === runId && value.sessionId === sessionId);
|
|
152
|
+
if (run === undefined) {
|
|
153
|
+
unknown('snapshot', 'The requested Run was absent from the Watch snapshot.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (message.payload?.session?.id === sessionId && message.payload?.progress?.runId === runId)
|
|
157
|
+
progress = message.payload.progress;
|
|
158
|
+
check(run);
|
|
159
|
+
if (!settled && until === 'approval' && progress === undefined) {
|
|
160
|
+
fail('NODE_OPERATION_UNSUPPORTED', 'errors.sessionApprovalUnsupported', {
|
|
161
|
+
stage: 'approval'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
checkApproval();
|
|
165
|
+
} else if (message.type === 'watch.session.event') {
|
|
166
|
+
const payload = message.event?.payload;
|
|
167
|
+
if (
|
|
168
|
+
(payload?.sessionId ?? payload?.run?.sessionId) === sessionId &&
|
|
169
|
+
payload?.progress?.runId === runId
|
|
170
|
+
)
|
|
171
|
+
progress = payload.progress;
|
|
172
|
+
check(payload?.run);
|
|
173
|
+
checkApproval();
|
|
174
|
+
}
|
|
175
|
+
if (!settled && message.expiresAt !== undefined) scheduleRenewal(message.expiresAt);
|
|
176
|
+
};
|
|
177
|
+
const onClose = (code, reason) => {
|
|
178
|
+
if (code === 1008 && String(reason) === 'UNAUTHENTICATED')
|
|
179
|
+
fail('UNAUTHENTICATED', 'errors.userContextUnavailable', {
|
|
180
|
+
statusCode: 401,
|
|
181
|
+
stage: 'connection'
|
|
182
|
+
});
|
|
183
|
+
else
|
|
184
|
+
unknown(
|
|
185
|
+
'connection',
|
|
186
|
+
Number.isInteger(code)
|
|
187
|
+
? `The Watch connection closed (code ${code}).`
|
|
188
|
+
: 'The Watch connection closed.'
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
const onError = (error) =>
|
|
192
|
+
unknown('connection', 'The Watch connection failed.', { code: error?.code });
|
|
193
|
+
const onOpen = () => {
|
|
194
|
+
try {
|
|
195
|
+
socket.send(JSON.stringify({ type: 'resume' }));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
onError(error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const onAbort = () =>
|
|
201
|
+
fail('MARCTL_WAIT_CANCELLED', 'errors.sessionWaitCancelled', { stage: 'cancel' });
|
|
202
|
+
try {
|
|
203
|
+
socket = client.openWorkbenchWebSocket(WebSocketClass ?? (await import('ws')).default);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof MarctlError) reject(error, 'connection');
|
|
206
|
+
else unknown('connection', 'The Watch connection could not be opened.');
|
|
207
|
+
return await result;
|
|
208
|
+
}
|
|
209
|
+
const timer = setTimeout(
|
|
210
|
+
() => fail('MARCTL_WAIT_TIMEOUT', 'errors.sessionWaitTimeout', { stage: 'timeout' }),
|
|
211
|
+
timeoutMs
|
|
212
|
+
);
|
|
213
|
+
socket.on('open', onOpen);
|
|
214
|
+
socket.on('message', onMessage);
|
|
215
|
+
socket.on('error', onError);
|
|
216
|
+
socket.on('close', onClose);
|
|
217
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
218
|
+
if (signal?.aborted) onAbort();
|
|
219
|
+
try {
|
|
220
|
+
return await result;
|
|
221
|
+
} finally {
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
clearTimeout(renewal);
|
|
225
|
+
signal?.removeEventListener('abort', onAbort);
|
|
226
|
+
if (subscribed) await command('stop').catch(() => undefined);
|
|
227
|
+
socket.off('open', onOpen);
|
|
228
|
+
socket.off('message', onMessage);
|
|
229
|
+
socket.off('close', onClose);
|
|
230
|
+
socket.close();
|
|
231
|
+
}
|
|
232
|
+
}
|