@myagentroam/marctl 0.10.5 → 0.10.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/marctl",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "description": "MyAgentRoam command-line interface for authorized Node access.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
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 writeLine(stderr, program + ': ' + failure.message + ' [' + failure.code + ']');
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(
@@ -46,19 +46,22 @@ Notes:
46
46
 
47
47
  Usage:
48
48
  {program} current [--json]
49
- {program} list [--cursor <cursor>] [--limit <1..100>] [--json]
49
+ {program} list [--query <title>] [--parent <session-id>]
50
+ [--cursor <cursor>] [--limit <1..100>] [--json]
50
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]
51
54
  {program} history <session-id>
52
55
  [--cursor <cursor>] [--limit <1..100>] [--json]
53
56
 
54
57
  {program} start --runner <runner>
55
58
  [-m <text> | --message-file <file|->] [--image <path> ...]
56
- [--model <id>] [--title <name>]
59
+ [--model <id>] [--effort <value>] [--title <name>]
57
60
  [--client-message-id <id>] [--json]
58
61
 
59
62
  {program} send <session-id>
60
63
  [-m <text> | --message-file <file|->] [--image <path> ...]
61
- [--delivery send|queue|replace-current]
64
+ (--steer | --queue)
62
65
  [--client-message-id <id>] [--json]
63
66
 
64
67
  {program} stop <session-id> [--run <run-id>] [--json]
@@ -74,10 +77,18 @@ Input:
74
77
  Files use UTF-8; - reads stdin. Repeat --image for PNG/JPEG/GIF/WebP files.
75
78
  Start/send allow image-only input: up to 5 images, 5 MiB each, 10 MiB total.
76
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.
77
82
 
78
83
  Delivery and waiting:
79
84
  Start/send return a clientMessageId.
80
- Send defaults to non-interrupting delivery. Queued runId may identify another
85
+ Send requires exactly one of --steer or --queue, even when idle.
86
+ When useful, --steer adds gentle context to an in-progress task.
87
+ --queue places input in a subsequent turn.
88
+ Example:
89
+ marctl sessions send <session-id> --steer \\
90
+ -m "Additional context: focus on the resume path." --json
91
+ A queued runId may identify another
81
92
  active input: correlate queueItemId/clientMessageId with visible history.
82
93
  Wait observes the specified Run through Watch. Timeout does not stop it;
83
94
  Run success is not proof of task success.
@@ -88,6 +99,11 @@ Credentials and scope:
88
99
  Credentials expire 24 hours after issue without automatic renewal.
89
100
  Scope is bound by MAR to the current Node/Workspace;
90
101
  there are no scope override options.
102
+ List and ID lookup use Node-known Sessions without scanning Runners.
103
+ --query filters display titles; missing IDs may suggest nearby IDs, not retry.
104
+ Start records the current Session as parent; no parent override.
105
+ --parent lists direct children; graph includes the root and descendants.
106
+ Graph defaults to 100 nodes and reports truncation, not task dependencies.
91
107
 
92
108
  Workers:
93
109
  Create workers as needed, e.g. --title "fixer#42 Handle empty totals" or
@@ -261,6 +277,7 @@ or Server connection is required.`
261
277
  terminalDirectUnsafeFallback:
262
278
  'Direct terminal setup could not be safely canceled after OPEN; relay fallback was skipped.',
263
279
  invalidArguments: 'Invalid command arguments.',
280
+ sessionDeliveryRequired: 'Send requires exactly one of --steer or --queue, even when idle.',
264
281
  sshOptionsUnsupported: 'SSH options are not supported; use marssh <host>.',
265
282
  scpOptionsUnsupported: 'This SCP option is not supported by the Node Terminal transport.',
266
283
  commandFailed: 'The command failed.'
@@ -45,19 +45,22 @@ ${configurationHelp}
45
45
 
46
46
  用法:
47
47
  {program} current [--json]
48
- {program} list [--cursor <cursor>] [--limit <1..100>] [--json]
48
+ {program} list [--query <title>] [--parent <session-id>]
49
+ [--cursor <cursor>] [--limit <1..100>] [--json]
49
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]
50
53
  {program} history <session-id>
51
54
  [--cursor <cursor>] [--limit <1..100>] [--json]
52
55
 
53
56
  {program} start --runner <runner>
54
57
  [-m <text> | --message-file <file|->] [--image <path> ...]
55
- [--model <id>] [--title <name>]
58
+ [--model <id>] [--effort <value>] [--title <name>]
56
59
  [--client-message-id <id>] [--json]
57
60
 
58
61
  {program} send <session-id>
59
62
  [-m <text> | --message-file <file|->] [--image <path> ...]
60
- [--delivery send|queue|replace-current]
63
+ (--steer | --queue)
61
64
  [--client-message-id <id>] [--json]
62
65
 
63
66
  {program} stop <session-id> [--run <run-id>] [--json]
@@ -73,10 +76,18 @@ ${configurationHelp}
73
76
  文件用 UTF-8,- 从 stdin 读取。
74
77
  start/send 可重复 --image 传 PNG/JPEG/GIF/WebP 图片,也可只发送图片。
75
78
  最多 5 张,单张 5 MiB、合计 10 MiB;goal set 只接受文本。
79
+ config 至少指定 model 或 effort;未指定字段和权限不变。
80
+ --effort "" 使用 Runner 默认值,不重启当前执行。
76
81
 
77
82
  投递与等待:
78
83
  start/send 返回 clientMessageId。
79
- send 默认不打断;排队回执的 runId 可能属于其他活动输入,
84
+ send 必须二选一:--steer 或 --queue,空闲时也需指定。
85
+ --steer 在必要时为正在进行的任务补充温和的新信息。
86
+ --queue 将输入排入后续轮次。
87
+ 示例:
88
+ marctl sessions send <session-id> --steer \\
89
+ -m "Additional context: focus on the resume path." --json
90
+ 排队回执的 runId 可能属于其他活动输入,
80
91
  需按 queueItemId/clientMessageId 与可见历史核对。
81
92
  wait 通过 Watch 等待指定 Run;超时不会停止任务,
82
93
  Run 成功不等于业务完成。Worker 完成不自动唤醒 Coordinator。
@@ -85,6 +96,11 @@ ${configurationHelp}
85
96
  凭据与范围:
86
97
  凭据自签发起 24 小时到期,不自动续签。
87
98
  MAR 绑定当前 Node/Workspace,不提供覆盖范围的参数。
99
+ 列表及 ID 定位只查 Node 已知会话,不扫描 Runner。
100
+ --query 按展示标题筛选;ID 不存在时可提示近似候选,不自动重试。
101
+ start 自动记录可用的当前会话为 parent,不提供 parent 覆盖参数。
102
+ --parent 查直接子会话;graph 查包含根的后代,默认最多 100 个节点,
103
+ 截断时明确标记;创建关系不表示任务依赖。
88
104
 
89
105
  Worker:
90
106
  按任务需要创建会话,例如 --title "fixer#42 Handle empty totals" 或
@@ -247,6 +263,7 @@ ${configurationHelp}`,
247
263
  terminalSessionFailed: '终端会话失败。',
248
264
  terminalDirectUnsafeFallback: 'OPEN 后无法安全取消直连终端,已跳过 relay 回退。',
249
265
  invalidArguments: '命令参数无效。',
266
+ sessionDeliveryRequired: 'send 必须且只能选择 --steer 或 --queue,即使目标空闲也需指定。',
250
267
  sshOptionsUnsupported: '不支持 SSH 选项,请使用 marssh <节点>。',
251
268
  scpOptionsUnsupported: 'Node Terminal 传输不支持此 SCP 选项。',
252
269
  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: ['runner', 'model', 'title', 'message', 'message-file', 'image', 'client-message-id'],
14
- send: ['message', 'message-file', 'image', 'delivery', 'client-message-id'],
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', 'steer', '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('errors.invalidArguments'), { code: 'MARCTL_SESSION_INPUT_INVALID' });
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 === '--steer' || 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 (value === undefined || value === '' || (name !== 'message' && value.startsWith('--')))
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.steer === 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 ['sessionId', 'runner', 'model', 'title', 'cursor', 'limit', 'runId'])
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 = { send: 'SEND', queue: 'QUEUE', 'replace-current': 'REPLACE_CURRENT' }[
138
- args.delivery ?? 'send'
139
- ];
178
+ body.deliveryIntent = args.steer === 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
- const socket = client.openWorkbenchWebSocket(WebSocketClass ?? (await import('ws')).default);
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
- reject(new MarctlError(translate(key), { ...options, code }));
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
- fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
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 fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
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
- fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
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
- fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
393
+ unknown('snapshot', 'The Watch snapshot did not contain a Run list.');
329
394
  return;
330
395
  }
331
- const run = runs.find((value) => value.id === runId && value.sessionId === sessionId);
396
+ const run = runs.find((value) => value?.id === runId && value.sessionId === sessionId);
332
397
  if (run === undefined) {
333
- fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
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', { statusCode: 401 });
343
- else fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown');
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
- onClose();
424
+ } catch (error) {
425
+ onError(error);
350
426
  }
351
427
  };
352
- const onAbort = () => fail('MARCTL_WAIT_CANCELLED', 'errors.sessionWaitCancelled');
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', onClose);
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();