@myagentroam/marctl 0.10.4 → 0.10.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/package.json +1 -1
- package/src/cli.mjs +8 -1
- package/src/i18n/en-US.mjs +72 -2
- package/src/i18n/zh-CN.mjs +69 -2
- package/src/sessions.mjs +116 -32
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -106,11 +106,18 @@ export async function main(
|
|
|
106
106
|
? {}
|
|
107
107
|
: { upstreamRequestId: error.upstreamRequestId }),
|
|
108
108
|
...(error.details === undefined ? {} : { details: error.details }),
|
|
109
|
+
...(error.sessionId === undefined ? {} : { sessionId: error.sessionId }),
|
|
110
|
+
...(error.runId === undefined ? {} : { runId: error.runId }),
|
|
111
|
+
...(error.stage === undefined ? {} : { stage: error.stage }),
|
|
112
|
+
...(error.upstreamCode === undefined ? {} : { upstreamCode: error.upstreamCode }),
|
|
109
113
|
...(error.clientMessageId === undefined ? {} : { clientMessageId: error.clientMessageId })
|
|
110
114
|
};
|
|
111
115
|
if (args?.json === true || (args === undefined && argv.includes('--json')))
|
|
112
116
|
writeLine(stdout, JSON.stringify({ error: failure }));
|
|
113
|
-
else
|
|
117
|
+
else {
|
|
118
|
+
writeLine(stderr, program + ': ' + failure.message + ' [' + failure.code + ']');
|
|
119
|
+
if (failure.details !== undefined) writeLine(stderr, failure.details);
|
|
120
|
+
}
|
|
114
121
|
return sessionExitCode(error);
|
|
115
122
|
}
|
|
116
123
|
writeLine(
|
package/src/i18n/en-US.mjs
CHANGED
|
@@ -42,8 +42,77 @@ Notes:
|
|
|
42
42
|
Each alias shows only its own command help. Use marctl help <command> for details.
|
|
43
43
|
Remote access uses authorized Node Terminal transport, not system SSH or SCP.`,
|
|
44
44
|
commandHelp: Object.freeze({
|
|
45
|
-
sessions:
|
|
46
|
-
|
|
45
|
+
sessions: `{program} — Coordinate Sessions in the current MAR Workspace
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
{program} current [--json]
|
|
49
|
+
{program} list [--query <title>] [--parent <session-id>]
|
|
50
|
+
[--cursor <cursor>] [--limit <1..100>] [--json]
|
|
51
|
+
{program} show <session-id> [--json]
|
|
52
|
+
{program} config <session-id> [--model <id>] [--effort <value>] [--json]
|
|
53
|
+
{program} graph <session-id> [--limit <1..100>] [--json]
|
|
54
|
+
{program} history <session-id>
|
|
55
|
+
[--cursor <cursor>] [--limit <1..100>] [--json]
|
|
56
|
+
|
|
57
|
+
{program} start --runner <runner>
|
|
58
|
+
[-m <text> | --message-file <file|->] [--image <path> ...]
|
|
59
|
+
[--model <id>] [--effort <value>] [--title <name>]
|
|
60
|
+
[--client-message-id <id>] [--json]
|
|
61
|
+
|
|
62
|
+
{program} send <session-id>
|
|
63
|
+
[-m <text> | --message-file <file|->] [--image <path> ...]
|
|
64
|
+
(--forced | --queue)
|
|
65
|
+
[--client-message-id <id>] [--json]
|
|
66
|
+
|
|
67
|
+
{program} stop <session-id> [--run <run-id>] [--json]
|
|
68
|
+
{program} goal get|clear <session-id> [--json]
|
|
69
|
+
{program} goal set <session-id>
|
|
70
|
+
(-m <text> | --message-file <file|->) [--json]
|
|
71
|
+
{program} wait <session-id>
|
|
72
|
+
--run <run-id> --timeout <30s|5m> [--json]
|
|
73
|
+
|
|
74
|
+
Input:
|
|
75
|
+
Runners: mar-agent, codex, claude-code, opencode; actual capabilities apply.
|
|
76
|
+
-m / --message supplies text; it is mutually exclusive with --message-file.
|
|
77
|
+
Files use UTF-8; - reads stdin. Repeat --image for PNG/JPEG/GIF/WebP files.
|
|
78
|
+
Start/send allow image-only input: up to 5 images, 5 MiB each, 10 MiB total.
|
|
79
|
+
Goal set accepts text only.
|
|
80
|
+
Config requires model or effort; other fields and permissions are unchanged.
|
|
81
|
+
Use --effort "" for the Runner default. Active execution is not restarted.
|
|
82
|
+
|
|
83
|
+
Delivery and waiting:
|
|
84
|
+
Start/send return a clientMessageId.
|
|
85
|
+
Send requires exactly one of --forced or --queue, even when idle.
|
|
86
|
+
--forced sends if idle; if busy, interrupts then prioritizes after release.
|
|
87
|
+
--queue waits its FIFO turn without interrupting; existing pauses remain.
|
|
88
|
+
Eligible queued input can start immediately when idle.
|
|
89
|
+
A queued runId may identify another
|
|
90
|
+
active input: correlate queueItemId/clientMessageId with visible history.
|
|
91
|
+
Wait observes the specified Run through Watch. Timeout does not stop it;
|
|
92
|
+
Run success is not proof of task success.
|
|
93
|
+
Completion never wakes a Coordinator.
|
|
94
|
+
On an unknown delivery outcome, inspect state/history; do not blindly resend.
|
|
95
|
+
|
|
96
|
+
Credentials and scope:
|
|
97
|
+
Credentials expire 24 hours after issue without automatic renewal.
|
|
98
|
+
Scope is bound by MAR to the current Node/Workspace;
|
|
99
|
+
there are no scope override options.
|
|
100
|
+
List and ID lookup use Node-known Sessions without scanning Runners.
|
|
101
|
+
--query filters display titles; missing IDs may suggest nearby IDs, not retry.
|
|
102
|
+
Start records the current Session as parent; no parent override.
|
|
103
|
+
--parent lists direct children; graph includes the root and descendants.
|
|
104
|
+
Graph defaults to 100 nodes and reports truncation, not task dependencies.
|
|
105
|
+
|
|
106
|
+
Workers:
|
|
107
|
+
Create workers as needed, e.g. --title "fixer#42 Handle empty totals" or
|
|
108
|
+
--title "reviewer#42 Review empty totals". Titles are not routing identities;
|
|
109
|
+
record returned Session IDs. Put task/role instructions in the first message.
|
|
110
|
+
Sessions share files and the Git index; no automatic worktree or worker pool.
|
|
111
|
+
|
|
112
|
+
Exit codes:
|
|
113
|
+
1 failure, 2 input, 3 authorization, 4 unsupported, 5 timeout,
|
|
114
|
+
6 unknown outcome, 130 cancelled wait.
|
|
115
|
+
--json prints one result/error object.`,
|
|
47
116
|
nodes: `{program} — List accessible Nodes
|
|
48
117
|
|
|
49
118
|
Usage:
|
|
@@ -206,6 +275,7 @@ or Server connection is required.`
|
|
|
206
275
|
terminalDirectUnsafeFallback:
|
|
207
276
|
'Direct terminal setup could not be safely canceled after OPEN; relay fallback was skipped.',
|
|
208
277
|
invalidArguments: 'Invalid command arguments.',
|
|
278
|
+
sessionDeliveryRequired: 'Send requires exactly one of --forced or --queue, even when idle.',
|
|
209
279
|
sshOptionsUnsupported: 'SSH options are not supported; use marssh <host>.',
|
|
210
280
|
scpOptionsUnsupported: 'This SCP option is not supported by the Node Terminal transport.',
|
|
211
281
|
commandFailed: 'The command failed.'
|
package/src/i18n/zh-CN.mjs
CHANGED
|
@@ -41,8 +41,74 @@ ${configurationHelp}
|
|
|
41
41
|
快捷命令仅显示对应操作的帮助。用 marctl help <子命令> 查看详细说明。
|
|
42
42
|
远程访问使用授权的 Node Terminal 传输,不是系统 SSH 或 SCP 协议。`,
|
|
43
43
|
commandHelp: Object.freeze({
|
|
44
|
-
sessions:
|
|
45
|
-
|
|
44
|
+
sessions: `{program} — 协调当前 MAR 工作区中的会话
|
|
45
|
+
|
|
46
|
+
用法:
|
|
47
|
+
{program} current [--json]
|
|
48
|
+
{program} list [--query <title>] [--parent <session-id>]
|
|
49
|
+
[--cursor <cursor>] [--limit <1..100>] [--json]
|
|
50
|
+
{program} show <session-id> [--json]
|
|
51
|
+
{program} config <session-id> [--model <id>] [--effort <value>] [--json]
|
|
52
|
+
{program} graph <session-id> [--limit <1..100>] [--json]
|
|
53
|
+
{program} history <session-id>
|
|
54
|
+
[--cursor <cursor>] [--limit <1..100>] [--json]
|
|
55
|
+
|
|
56
|
+
{program} start --runner <runner>
|
|
57
|
+
[-m <text> | --message-file <file|->] [--image <path> ...]
|
|
58
|
+
[--model <id>] [--effort <value>] [--title <name>]
|
|
59
|
+
[--client-message-id <id>] [--json]
|
|
60
|
+
|
|
61
|
+
{program} send <session-id>
|
|
62
|
+
[-m <text> | --message-file <file|->] [--image <path> ...]
|
|
63
|
+
(--forced | --queue)
|
|
64
|
+
[--client-message-id <id>] [--json]
|
|
65
|
+
|
|
66
|
+
{program} stop <session-id> [--run <run-id>] [--json]
|
|
67
|
+
{program} goal get|clear <session-id> [--json]
|
|
68
|
+
{program} goal set <session-id>
|
|
69
|
+
(-m <text> | --message-file <file|->) [--json]
|
|
70
|
+
{program} wait <session-id>
|
|
71
|
+
--run <run-id> --timeout <30s|5m> [--json]
|
|
72
|
+
|
|
73
|
+
输入:
|
|
74
|
+
Runner:mar-agent、codex、claude-code、opencode,操作以实际能力为准。
|
|
75
|
+
-m / --message 直接传文本,与 --message-file 互斥;
|
|
76
|
+
文件用 UTF-8,- 从 stdin 读取。
|
|
77
|
+
start/send 可重复 --image 传 PNG/JPEG/GIF/WebP 图片,也可只发送图片。
|
|
78
|
+
最多 5 张,单张 5 MiB、合计 10 MiB;goal set 只接受文本。
|
|
79
|
+
config 至少指定 model 或 effort;未指定字段和权限不变。
|
|
80
|
+
--effort "" 使用 Runner 默认值,不重启当前执行。
|
|
81
|
+
|
|
82
|
+
投递与等待:
|
|
83
|
+
start/send 返回 clientMessageId。
|
|
84
|
+
send 必须二选一:--forced 或 --queue,空闲时也需指定。
|
|
85
|
+
--forced 空闲发送;忙时打断当前执行,释放后优先处理。
|
|
86
|
+
send --queue 不打断,等当前执行释放后按 FIFO 处理,保留暂停状态。
|
|
87
|
+
队列空闲且可执行时可立即开始。
|
|
88
|
+
排队回执的 runId 可能属于其他活动输入,
|
|
89
|
+
需按 queueItemId/clientMessageId 与可见历史核对。
|
|
90
|
+
wait 通过 Watch 等待指定 Run;超时不会停止任务,
|
|
91
|
+
Run 成功不等于业务完成。Worker 完成不自动唤醒 Coordinator。
|
|
92
|
+
投递结果未知时先查状态/历史,不盲目重发。
|
|
93
|
+
|
|
94
|
+
凭据与范围:
|
|
95
|
+
凭据自签发起 24 小时到期,不自动续签。
|
|
96
|
+
MAR 绑定当前 Node/Workspace,不提供覆盖范围的参数。
|
|
97
|
+
列表及 ID 定位只查 Node 已知会话,不扫描 Runner。
|
|
98
|
+
--query 按展示标题筛选;ID 不存在时可提示近似候选,不自动重试。
|
|
99
|
+
start 自动记录可用的当前会话为 parent,不提供 parent 覆盖参数。
|
|
100
|
+
--parent 查直接子会话;graph 查包含根的后代,默认最多 100 个节点,
|
|
101
|
+
截断时明确标记;创建关系不表示任务依赖。
|
|
102
|
+
|
|
103
|
+
Worker:
|
|
104
|
+
按任务需要创建会话,例如 --title "fixer#42 Handle empty totals" 或
|
|
105
|
+
--title "reviewer#42 Review empty totals";标题不是寻址身份,
|
|
106
|
+
保存返回的会话 ID。角色和任务要求放入首条普通消息。
|
|
107
|
+
会话共享文件与 Git 索引,不自动隔离工作目录或维护池。
|
|
108
|
+
|
|
109
|
+
退出码:
|
|
110
|
+
1 失败,2 参数,3 授权,4 不支持,5 超时,6 结果未知,130 取消等待。
|
|
111
|
+
--json 只输出一个结果或错误对象。`,
|
|
46
112
|
nodes: `{program} — 列出有权限的节点
|
|
47
113
|
|
|
48
114
|
用法:
|
|
@@ -195,6 +261,7 @@ ${configurationHelp}`,
|
|
|
195
261
|
terminalSessionFailed: '终端会话失败。',
|
|
196
262
|
terminalDirectUnsafeFallback: 'OPEN 后无法安全取消直连终端,已跳过 relay 回退。',
|
|
197
263
|
invalidArguments: '命令参数无效。',
|
|
264
|
+
sessionDeliveryRequired: 'send 必须且只能选择 --forced 或 --queue,即使目标空闲也需指定。',
|
|
198
265
|
sshOptionsUnsupported: '不支持 SSH 选项,请使用 marssh <节点>。',
|
|
199
266
|
scpOptionsUnsupported: 'Node Terminal 传输不支持此 SCP 选项。',
|
|
200
267
|
terminalBinaryUnavailable: '二进制复制需要支持 pipe 终端的 Server 与 Node。',
|
package/src/sessions.mjs
CHANGED
|
@@ -7,11 +7,22 @@ import { createTranslator } from './i18n/index.mjs';
|
|
|
7
7
|
const terminal = new Set(['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED']);
|
|
8
8
|
const commands = {
|
|
9
9
|
current: [],
|
|
10
|
-
list: ['cursor', 'limit'],
|
|
10
|
+
list: ['cursor', 'limit', 'query', 'parent'],
|
|
11
|
+
graph: ['limit'],
|
|
11
12
|
show: [],
|
|
12
13
|
history: ['cursor', 'limit'],
|
|
13
|
-
start: [
|
|
14
|
-
|
|
14
|
+
start: [
|
|
15
|
+
'runner',
|
|
16
|
+
'model',
|
|
17
|
+
'effort',
|
|
18
|
+
'title',
|
|
19
|
+
'message',
|
|
20
|
+
'message-file',
|
|
21
|
+
'image',
|
|
22
|
+
'client-message-id'
|
|
23
|
+
],
|
|
24
|
+
config: ['model', 'effort'],
|
|
25
|
+
send: ['message', 'message-file', 'image', 'forced', 'queue', 'client-message-id'],
|
|
15
26
|
stop: ['run'],
|
|
16
27
|
'goal.get': [],
|
|
17
28
|
'goal.set': ['message', 'message-file'],
|
|
@@ -19,6 +30,7 @@ const commands = {
|
|
|
19
30
|
wait: ['run', 'timeout']
|
|
20
31
|
};
|
|
21
32
|
const fieldNames = {
|
|
33
|
+
parent: 'parentSessionId',
|
|
22
34
|
'message-file': 'messageFile',
|
|
23
35
|
'client-message-id': 'clientMessageId',
|
|
24
36
|
run: 'runId',
|
|
@@ -27,8 +39,8 @@ const fieldNames = {
|
|
|
27
39
|
};
|
|
28
40
|
|
|
29
41
|
export function parseSessionArgs(argv, translate = createTranslator()) {
|
|
30
|
-
const invalid = () =>
|
|
31
|
-
new MarctlError(translate(
|
|
42
|
+
const invalid = (key = 'errors.invalidArguments') =>
|
|
43
|
+
new MarctlError(translate(key), { code: 'MARCTL_SESSION_INPUT_INVALID' });
|
|
32
44
|
if (
|
|
33
45
|
argv.length === 0 ||
|
|
34
46
|
argv.some(
|
|
@@ -54,12 +66,22 @@ export function parseSessionArgs(argv, translate = createTranslator()) {
|
|
|
54
66
|
args.json = true;
|
|
55
67
|
continue;
|
|
56
68
|
}
|
|
69
|
+
if (action === 'send' && (flag === '--forced' || flag === '--queue')) {
|
|
70
|
+
const key = flag.slice(2);
|
|
71
|
+
if (Object.hasOwn(args, key)) throw invalid();
|
|
72
|
+
args[key] = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
57
75
|
const name = flag.startsWith('--') ? flag.slice(2) : '';
|
|
58
76
|
const key = fieldNames[name] ?? name;
|
|
59
77
|
if (!commands[action].includes(name) || (name !== 'image' && Object.hasOwn(args, key)))
|
|
60
78
|
throw invalid();
|
|
61
79
|
const value = input.shift();
|
|
62
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
value === undefined ||
|
|
82
|
+
(value === '' && name !== 'effort') ||
|
|
83
|
+
(name !== 'message' && value.startsWith('--'))
|
|
84
|
+
)
|
|
63
85
|
throw invalid();
|
|
64
86
|
if (name === 'image') {
|
|
65
87
|
(args.images ??= []).push(value);
|
|
@@ -74,6 +96,8 @@ export function parseSessionArgs(argv, translate = createTranslator()) {
|
|
|
74
96
|
args[key] = ms;
|
|
75
97
|
} else args[key] = value;
|
|
76
98
|
}
|
|
99
|
+
if (action === 'send' && (args.forced === true) === (args.queue === true))
|
|
100
|
+
throw invalid('errors.sessionDeliveryRequired');
|
|
77
101
|
if (args.message !== undefined && args.messageFile !== undefined) throw invalid();
|
|
78
102
|
if (
|
|
79
103
|
['start', 'send', 'goal.set'].includes(action) &&
|
|
@@ -89,9 +113,13 @@ export function parseSessionArgs(argv, translate = createTranslator()) {
|
|
|
89
113
|
throw invalid();
|
|
90
114
|
if (action === 'wait' && (args.runId === undefined || args.timeoutMs === undefined))
|
|
91
115
|
throw invalid();
|
|
92
|
-
if (args.delivery !== undefined && !['send', 'queue', 'replace-current'].includes(args.delivery))
|
|
93
|
-
throw invalid();
|
|
94
116
|
if (args.title !== undefined && args.title.length > 160) throw invalid();
|
|
117
|
+
if (args.model !== undefined && (args.model.trim().length === 0 || args.model.length > 120))
|
|
118
|
+
throw invalid();
|
|
119
|
+
if (args.effort !== undefined && args.effort.length > 120) throw invalid();
|
|
120
|
+
if (action === 'config' && args.model === undefined && args.effort === undefined) throw invalid();
|
|
121
|
+
if (args.query !== undefined && args.query.length > 160) throw invalid();
|
|
122
|
+
if (args.parentSessionId !== undefined && args.parentSessionId.length > 512) throw invalid();
|
|
95
123
|
return args;
|
|
96
124
|
}
|
|
97
125
|
|
|
@@ -117,9 +145,22 @@ export async function runSessionCommand({ args, client, env, stdin, WebSocketCla
|
|
|
117
145
|
}
|
|
118
146
|
}
|
|
119
147
|
const body = { action: args.action };
|
|
120
|
-
for (const name of [
|
|
148
|
+
for (const name of [
|
|
149
|
+
'sessionId',
|
|
150
|
+
'parentSessionId',
|
|
151
|
+
'runner',
|
|
152
|
+
'model',
|
|
153
|
+
'effort',
|
|
154
|
+
'title',
|
|
155
|
+
'cursor',
|
|
156
|
+
'limit',
|
|
157
|
+
'query',
|
|
158
|
+
'runId'
|
|
159
|
+
])
|
|
121
160
|
if (args[name] !== undefined) body[name] = args[name];
|
|
122
161
|
if (args.action === 'current' && env.MAR_CTL_SESSION_ID) body.sessionId = env.MAR_CTL_SESSION_ID;
|
|
162
|
+
if (args.action === 'start' && env.MAR_CTL_SESSION_ID)
|
|
163
|
+
body.parentSessionId = env.MAR_CTL_SESSION_ID;
|
|
123
164
|
if (args.messageFile !== undefined || args.message !== undefined)
|
|
124
165
|
body.content = await messageContent(
|
|
125
166
|
args,
|
|
@@ -134,9 +175,7 @@ export async function runSessionCommand({ args, client, env, stdin, WebSocketCla
|
|
|
134
175
|
if (args.action === 'start' || args.action === 'send')
|
|
135
176
|
body.clientMessageId = args.clientMessageId ?? randomUUID();
|
|
136
177
|
if (args.action === 'send')
|
|
137
|
-
body.deliveryIntent =
|
|
138
|
-
args.delivery ?? 'send'
|
|
139
|
-
];
|
|
178
|
+
body.deliveryIntent = args.forced === true ? 'REPLACE_CURRENT' : 'QUEUE';
|
|
140
179
|
try {
|
|
141
180
|
const result = await client.sessionCommand(body);
|
|
142
181
|
return {
|
|
@@ -148,7 +187,7 @@ export async function runSessionCommand({ args, client, env, stdin, WebSocketCla
|
|
|
148
187
|
} catch (error) {
|
|
149
188
|
if (body.clientMessageId !== undefined) error.clientMessageId = body.clientMessageId;
|
|
150
189
|
if (
|
|
151
|
-
['start', 'send', 'stop', 'goal.set', 'goal.clear'].includes(args.action) &&
|
|
190
|
+
['start', 'send', 'config', 'stop', 'goal.set', 'goal.clear'].includes(args.action) &&
|
|
152
191
|
['MARCTL_REQUEST_TIMEOUT', 'MARCTL_SERVER_UNREACHABLE', 'MARCTL_RESPONSE_INVALID'].includes(
|
|
153
192
|
error.code
|
|
154
193
|
)
|
|
@@ -244,7 +283,7 @@ export async function waitForSessionRun({
|
|
|
244
283
|
translate = createTranslator(),
|
|
245
284
|
signal
|
|
246
285
|
}) {
|
|
247
|
-
|
|
286
|
+
let socket;
|
|
248
287
|
const watchId = randomUUID();
|
|
249
288
|
let connectionId;
|
|
250
289
|
let renewal;
|
|
@@ -256,13 +295,34 @@ export async function waitForSessionRun({
|
|
|
256
295
|
accept = resolve;
|
|
257
296
|
rejectResult = fail;
|
|
258
297
|
});
|
|
259
|
-
const reject = (error) => {
|
|
298
|
+
const reject = (error, stage) => {
|
|
260
299
|
if (settled) return;
|
|
261
300
|
settled = true;
|
|
301
|
+
Object.assign(error, { sessionId, runId, stage });
|
|
262
302
|
rejectResult(error);
|
|
263
303
|
};
|
|
264
|
-
const fail = (code, key, options = {}) =>
|
|
265
|
-
|
|
304
|
+
const fail = (code, key, { stage, upstreamCode, ...options } = {}) => {
|
|
305
|
+
const error = new MarctlError(translate(key), { ...options, code });
|
|
306
|
+
if (upstreamCode !== undefined) error.upstreamCode = upstreamCode;
|
|
307
|
+
reject(error, stage);
|
|
308
|
+
};
|
|
309
|
+
const unknown = (stage, detail, source) =>
|
|
310
|
+
fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown', {
|
|
311
|
+
stage,
|
|
312
|
+
upstreamCode:
|
|
313
|
+
typeof source?.code === 'string' && /^[A-Za-z][A-Za-z0-9_]{0,127}$/u.test(source.code)
|
|
314
|
+
? source.code
|
|
315
|
+
: undefined,
|
|
316
|
+
requestId: typeof source?.requestId === 'string' ? source.requestId.slice(0, 160) : undefined,
|
|
317
|
+
upstreamRequestId:
|
|
318
|
+
typeof source?.upstreamRequestId === 'string'
|
|
319
|
+
? source.upstreamRequestId.slice(0, 160)
|
|
320
|
+
: undefined,
|
|
321
|
+
details: [detail, source?.message, source?.details]
|
|
322
|
+
.filter((value) => typeof value === 'string')
|
|
323
|
+
.join(' ')
|
|
324
|
+
.slice(0, 4000)
|
|
325
|
+
});
|
|
266
326
|
const command = (mode) =>
|
|
267
327
|
client.sessionCommand({
|
|
268
328
|
action: 'watch',
|
|
@@ -275,7 +335,7 @@ export async function waitForSessionRun({
|
|
|
275
335
|
clearTimeout(renewal);
|
|
276
336
|
const remaining = expiresAt - Date.now();
|
|
277
337
|
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
278
|
-
|
|
338
|
+
unknown('lease', 'The Watch lease is missing or expired.');
|
|
279
339
|
return;
|
|
280
340
|
}
|
|
281
341
|
renewal = setTimeout(
|
|
@@ -285,8 +345,13 @@ export async function waitForSessionRun({
|
|
|
285
345
|
if (!settled) scheduleRenewal(ack?.expiresAt);
|
|
286
346
|
})
|
|
287
347
|
.catch((error) => {
|
|
288
|
-
if ([401, 403].includes(error.statusCode)) reject(error);
|
|
289
|
-
else
|
|
348
|
+
if ([401, 403].includes(error.statusCode)) reject(error, 'renew');
|
|
349
|
+
else
|
|
350
|
+
unknown(
|
|
351
|
+
'renew',
|
|
352
|
+
'The Watch lease could not be renewed.',
|
|
353
|
+
error instanceof MarctlError ? error : undefined
|
|
354
|
+
);
|
|
290
355
|
});
|
|
291
356
|
},
|
|
292
357
|
Math.max(50, Math.floor(remaining / 2))
|
|
@@ -315,22 +380,22 @@ export async function waitForSessionRun({
|
|
|
315
380
|
) {
|
|
316
381
|
connectionId = message.workbenchConnectionId;
|
|
317
382
|
subscribed = true;
|
|
318
|
-
command('initial').catch(reject);
|
|
383
|
+
command('initial').catch((error) => reject(error, 'subscribe'));
|
|
319
384
|
}
|
|
320
385
|
if (message.watchId !== watchId) return;
|
|
321
386
|
if (message.type === 'watch.failed') {
|
|
322
|
-
|
|
387
|
+
unknown('watch', 'The Session Watch failed.', message);
|
|
323
388
|
return;
|
|
324
389
|
}
|
|
325
390
|
if (message.type === 'watch.session.snapshot') {
|
|
326
391
|
const runs = message.payload?.runs;
|
|
327
392
|
if (!Array.isArray(runs)) {
|
|
328
|
-
|
|
393
|
+
unknown('snapshot', 'The Watch snapshot did not contain a Run list.');
|
|
329
394
|
return;
|
|
330
395
|
}
|
|
331
|
-
const run = runs.find((value) => value
|
|
396
|
+
const run = runs.find((value) => value?.id === runId && value.sessionId === sessionId);
|
|
332
397
|
if (run === undefined) {
|
|
333
|
-
|
|
398
|
+
unknown('snapshot', 'The requested Run was absent from the Watch snapshot.');
|
|
334
399
|
return;
|
|
335
400
|
}
|
|
336
401
|
check(run);
|
|
@@ -339,24 +404,43 @@ export async function waitForSessionRun({
|
|
|
339
404
|
};
|
|
340
405
|
const onClose = (code, reason) => {
|
|
341
406
|
if (code === 1008 && String(reason) === 'UNAUTHENTICATED')
|
|
342
|
-
fail('UNAUTHENTICATED', 'errors.userContextUnavailable', {
|
|
343
|
-
|
|
407
|
+
fail('UNAUTHENTICATED', 'errors.userContextUnavailable', {
|
|
408
|
+
statusCode: 401,
|
|
409
|
+
stage: 'connection'
|
|
410
|
+
});
|
|
411
|
+
else
|
|
412
|
+
unknown(
|
|
413
|
+
'connection',
|
|
414
|
+
Number.isInteger(code)
|
|
415
|
+
? `The Watch connection closed (code ${code}).`
|
|
416
|
+
: 'The Watch connection closed.'
|
|
417
|
+
);
|
|
344
418
|
};
|
|
419
|
+
const onError = (error) =>
|
|
420
|
+
unknown('connection', 'The Watch connection failed.', { code: error?.code });
|
|
345
421
|
const onOpen = () => {
|
|
346
422
|
try {
|
|
347
423
|
socket.send(JSON.stringify({ type: 'resume' }));
|
|
348
|
-
} catch {
|
|
349
|
-
|
|
424
|
+
} catch (error) {
|
|
425
|
+
onError(error);
|
|
350
426
|
}
|
|
351
427
|
};
|
|
352
|
-
const onAbort = () =>
|
|
428
|
+
const onAbort = () =>
|
|
429
|
+
fail('MARCTL_WAIT_CANCELLED', 'errors.sessionWaitCancelled', { stage: 'cancel' });
|
|
430
|
+
try {
|
|
431
|
+
socket = client.openWorkbenchWebSocket(WebSocketClass ?? (await import('ws')).default);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof MarctlError) reject(error, 'connection');
|
|
434
|
+
else unknown('connection', 'The Watch connection could not be opened.');
|
|
435
|
+
return await result;
|
|
436
|
+
}
|
|
353
437
|
const timer = setTimeout(
|
|
354
|
-
() => fail('MARCTL_WAIT_TIMEOUT', 'errors.sessionWaitTimeout'),
|
|
438
|
+
() => fail('MARCTL_WAIT_TIMEOUT', 'errors.sessionWaitTimeout', { stage: 'timeout' }),
|
|
355
439
|
timeoutMs
|
|
356
440
|
);
|
|
357
441
|
socket.on('open', onOpen);
|
|
358
442
|
socket.on('message', onMessage);
|
|
359
|
-
socket.on('error',
|
|
443
|
+
socket.on('error', onError);
|
|
360
444
|
socket.on('close', onClose);
|
|
361
445
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
362
446
|
if (signal?.aborted) onAbort();
|