@myagentroam/node 0.10.46 → 0.10.48
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.js +2 -1
- package/dist/runner/mar-agent/managed-run-controller.js +2 -1
- package/dist/runner/mar-agent/marctl-skill.js +7 -3
- package/dist/service/schedule-mcp-bridge.js +8 -22
- package/dist/service/scheduled-session-store.js +56 -0
- package/dist/service/session-schedule-service.js +55 -2
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -1619,7 +1619,8 @@ export class NodeConnector {
|
|
|
1619
1619
|
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1620
1620
|
session: this.sessionPresentationService.present(session)
|
|
1621
1621
|
}),
|
|
1622
|
-
changed: () => this.scheduledSessions.changed()
|
|
1622
|
+
changed: () => this.scheduledSessions.changed(),
|
|
1623
|
+
proposed: (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId)
|
|
1623
1624
|
});
|
|
1624
1625
|
this.operationRouter.registerAll(this.sessionScheduleService.operations());
|
|
1625
1626
|
this.operationRouter.registerAll(this.sessionMessageService.operations());
|
|
@@ -320,8 +320,10 @@ export class MarAgentManagedRunController {
|
|
|
320
320
|
runtime.activeRunId = input.runId;
|
|
321
321
|
await runtime.mcpLease.pool.refreshForExecution();
|
|
322
322
|
controller.signal.throwIfAborted();
|
|
323
|
+
const marctlSkill = marctlHostSkill(input.secretEnvironment);
|
|
323
324
|
const executionMcpServers = input.mcpServers
|
|
324
325
|
.filter(isExecutionScopedMcp)
|
|
326
|
+
.filter((server) => marctlSkill === undefined || server.name !== NODE_MCP.schedule.name)
|
|
325
327
|
.map((server) => ({ ...server, lifecycle: 'EXECUTION' }));
|
|
326
328
|
computerUseBinding = await this.#computerUse?.bind({
|
|
327
329
|
runId: input.runId,
|
|
@@ -371,7 +373,6 @@ export class MarAgentManagedRunController {
|
|
|
371
373
|
? {}
|
|
372
374
|
: { webFetch: (request, signal) => this.host.webFetch(request, signal) })
|
|
373
375
|
}));
|
|
374
|
-
const marctlSkill = marctlHostSkill(input.secretEnvironment);
|
|
375
376
|
executionMcpBinding = runtime.host.bindExecutionExtensions({
|
|
376
377
|
...extensions,
|
|
377
378
|
environment: marctlExecutionEnvironment(input.secretEnvironment),
|
|
@@ -10,7 +10,7 @@ export function marctlHostSkill(environment = {}) {
|
|
|
10
10
|
return undefined;
|
|
11
11
|
return {
|
|
12
12
|
name: 'marctl',
|
|
13
|
-
description: 'You are in a MAR-managed Workspace with marctl on PATH. Use this skill to inspect, create, message, stop or coordinate independent Sessions, or use authorized Node terminals and file transfer. Load before cross-Session coordination.',
|
|
13
|
+
description: 'You are in a MAR-managed Workspace with marctl on PATH. Use this skill to inspect, create, rename, configure, message, stop or coordinate independent Sessions, manage their scheduled tasks, or use authorized Node terminals and file transfer. Load before cross-Session coordination.',
|
|
14
14
|
requiredTools: ['exec'],
|
|
15
15
|
load: async () => marctlInstructions
|
|
16
16
|
};
|
|
@@ -26,8 +26,12 @@ const marctlInstructions = [
|
|
|
26
26
|
'For start/send, repeat --image <local-path> to attach PNG, JPEG, GIF or WebP images, with or without text. Images are read on this CLI host, not by remote path. Limits: 5 images, 5 MiB each, 10 MiB total. Actual Runner/model image capabilities apply; goal set accepts text only.',
|
|
27
27
|
'Create workers only as tasks need them, named fixer#<task-id> <description> or reviewer#<task-or-change-id> <description>. These are conventions, not product roles or a fixed pool. Put role, scope, acceptance criteria and delivery requirements in the first message. Follow the repository workflow for development and review. Session Prompt is edited in the Web UI only; marctl has no editing command, and sends to existing Sessions use normal prompt injection.',
|
|
28
28
|
'Reuse recorded Session/Run IDs and receipts. Refresh volatile Run, queue and Goal state with sessions show only when the next action depends on it. Select needed JSON fields instead of dumping full Session lists or tool transcripts. Hidden reasoning, credentials and raw model requests are not collaboration artifacts.',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
29
|
+
'show --json fields: session.activity, session.model, session.effort, session.customTitle or session.runnerTitle, runs[] (id/status), queue.items[], and goal.status (goal may be null). Run status and Goal status are separate.',
|
|
30
|
+
'Rename any authorized Workspace Session with sessions rename <session-id> --title <name> --json; parent relationships do not restrict renaming.',
|
|
31
|
+
'show/config/history/graph/rename and schedule get/create/update may omit the Session ID to use the current Session. Without a current Session, specify an ID. send/goal/stop/wait always require explicit target IDs.',
|
|
32
|
+
'Use sessions schedule list/get/create/update for the same tasks as the built-in schedule MCP, which is hidden while this skill is available. list is Workspace-scoped. create requires --cron and text (-m/--message-file, up to 20,000 characters). update requires --expected-revision from get and at least cron or text. Other-owner edits return a PENDING proposal, not an applied change; scheduled execution is read-only. Follow existing approval rules and do not retry conflicts or uncertain writes blindly.',
|
|
33
|
+
'Read results with sessions history <session-id> --limit 10 --json (default: latest 10 SEGMENTs, not messages; --limit 1..100). In segments[].items[], select kind === "assistant_message" and read payload.text; keep segment runId and user_message.payload.clientMessageId for queued-input correlation. Reuse the returned page; follow --cursor <nextCursor> only for missing evidence.',
|
|
34
|
+
'Use sessions send <session-id> --steer -m "Additional context: focus on the resume path." --json for decision-changing updates rather than routine reports: it may switch the current execution. Node decides delivery from actual state; an idle Session receives ordinary input. Choose exactly one of --steer or --queue, even when idle; missing, conflicting, repeated, or obsolete options are rejected locally before input is read or a request is sent. Use --queue to place input in a subsequent turn; with an active Goal, ordinary queued input waits until the Goal exits. start/send return clientMessageId and the accepted/queued result. A queued result may refer to the currently active Run: associate queueItemId/clientMessageId with history before deciding that input finished. There are no queue-item edit/delete/reorder commands; show includes queue state.',
|
|
31
35
|
'Wait with sessions wait <session-id> --run <run-id> --timeout 5m --json through the existing Watch. Prefer --timeout 5m when blocked on task completion; use 30s for a short deadline. Continue useful independent work before waiting. If exec returns a running process or cell, reuse its process/cell handle rather than starting another wait or polling show/list. Timeout or cancelled waiting does not stop the task. Use sessions stop <session-id> --run <run-id> --json only to stop that active Run; ownership rules still apply.',
|
|
32
36
|
'Goal get/set/clear use the existing Session Goal semantics; see marctl help sessions. A Coordinator can work from ordinary messages or its existing schedule without a Goal. Worker completion never automatically wakes it. Actively wait/read results, or finish and reconcile on the next user message or scheduled input.',
|
|
33
37
|
'## Verify, recover and respect authorization',
|
|
@@ -139,32 +139,18 @@ export class ScheduleMcpBridge {
|
|
|
139
139
|
if (task === undefined)
|
|
140
140
|
throw new Error('SCHEDULE_NOT_FOUND');
|
|
141
141
|
const args = record(request.arguments);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const patch = {
|
|
149
|
-
...(objective === undefined ? {} : { objective: objective }),
|
|
150
|
-
...(cron === undefined ? {} : { cron: cron })
|
|
151
|
-
};
|
|
152
|
-
if (capability.actorUserId === task.ownerUserId) {
|
|
153
|
-
const updated = this.store().update(task.sessionId, task.revision, {
|
|
154
|
-
...patch,
|
|
155
|
-
ownerUserId: capability.actorUserId
|
|
156
|
-
});
|
|
142
|
+
const result = this.store().updateFromAgent(task.sessionId, task.revision, capability.actorUserId, args);
|
|
143
|
+
if ('proposalId' in result) {
|
|
144
|
+
this.proposed(task.sessionId, result.proposalId);
|
|
145
|
+
socket.end(`${JSON.stringify({ result })}\n`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
157
148
|
this.#capabilities.set(request.token, {
|
|
158
149
|
...capability,
|
|
159
|
-
revision:
|
|
150
|
+
revision: result.revision
|
|
160
151
|
});
|
|
161
152
|
this.changed(task.sessionId);
|
|
162
|
-
socket.end(`${JSON.stringify({ result
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const proposalId = this.store().propose(task, capability.actorUserId, patch);
|
|
166
|
-
this.proposed(task.sessionId, proposalId);
|
|
167
|
-
socket.end(`${JSON.stringify({ result: { proposalId, status: 'PENDING' } })}\n`);
|
|
153
|
+
socket.end(`${JSON.stringify({ result })}\n`);
|
|
168
154
|
}
|
|
169
155
|
}
|
|
170
156
|
catch (error) {
|
|
@@ -25,6 +25,62 @@ export class ScheduledSessionStore {
|
|
|
25
25
|
.prepare('SELECT * FROM scheduled_session_tasks WHERE enabled=1 AND next_run_at<=? ORDER BY next_run_at,id')
|
|
26
26
|
.all(at).map(taskFromRow);
|
|
27
27
|
}
|
|
28
|
+
list(workspaceId, cursor, limit = 20) {
|
|
29
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
30
|
+
throw new Error('SCHEDULE_INVALID');
|
|
31
|
+
let after = '';
|
|
32
|
+
if (cursor !== undefined) {
|
|
33
|
+
try {
|
|
34
|
+
if (cursor.length > 4096)
|
|
35
|
+
throw new Error();
|
|
36
|
+
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
37
|
+
if (decoded.workspaceId !== workspaceId ||
|
|
38
|
+
typeof decoded.after !== 'string' ||
|
|
39
|
+
decoded.after.length < 1 ||
|
|
40
|
+
decoded.after.length > 512)
|
|
41
|
+
throw new Error();
|
|
42
|
+
after = decoded.after;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error('SCHEDULE_INVALID');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const rows = this.database
|
|
49
|
+
.prepare(`SELECT t.*, COALESCE(json_extract(s.metadata,'$.customTitle'),
|
|
50
|
+
json_extract(s.metadata,'$.runnerTitle'),s.id) AS session_title
|
|
51
|
+
FROM scheduled_session_tasks t JOIN agent_sessions s ON s.id=t.session_id
|
|
52
|
+
WHERE t.workspace_id=? AND t.id>? ORDER BY t.id LIMIT ?`)
|
|
53
|
+
.all(workspaceId, after, limit + 1);
|
|
54
|
+
const tasks = rows
|
|
55
|
+
.slice(0, limit)
|
|
56
|
+
.map((row) => ({ ...taskFromRow(row), sessionTitle: row['session_title'] }));
|
|
57
|
+
return {
|
|
58
|
+
tasks,
|
|
59
|
+
nextCursor: rows.length > limit
|
|
60
|
+
? Buffer.from(JSON.stringify({ workspaceId, after: tasks.at(-1).id })).toString('base64url')
|
|
61
|
+
: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
updateFromAgent(sessionId, expectedRevision, actorUserId, input) {
|
|
65
|
+
if (!Number.isSafeInteger(actorUserId) || actorUserId < 1)
|
|
66
|
+
throw new Error('USER_AUTHORIZATION_REQUIRED');
|
|
67
|
+
const task = this.get(sessionId);
|
|
68
|
+
if (task === undefined)
|
|
69
|
+
throw new Error('SCHEDULE_NOT_FOUND');
|
|
70
|
+
if (task.revision !== expectedRevision)
|
|
71
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
72
|
+
if ((input.objective !== undefined && typeof input.objective !== 'string') ||
|
|
73
|
+
(input.cron !== undefined && typeof input.cron !== 'string') ||
|
|
74
|
+
(input.objective === undefined && input.cron === undefined))
|
|
75
|
+
throw new Error('SCHEDULE_UPDATE_INVALID');
|
|
76
|
+
const patch = {
|
|
77
|
+
...(input.objective === undefined ? {} : { objective: input.objective }),
|
|
78
|
+
...(input.cron === undefined ? {} : { cron: input.cron })
|
|
79
|
+
};
|
|
80
|
+
if (actorUserId === task.ownerUserId)
|
|
81
|
+
return this.update(sessionId, expectedRevision, patch);
|
|
82
|
+
return { proposalId: this.propose(task, actorUserId, patch), status: 'PENDING' };
|
|
83
|
+
}
|
|
28
84
|
nextDueAt() {
|
|
29
85
|
const row = this.database
|
|
30
86
|
.prepare('SELECT MIN(next_run_at) AS value FROM scheduled_session_tasks WHERE enabled=1')
|
|
@@ -21,11 +21,64 @@ export class SessionScheduleService {
|
|
|
21
21
|
operations() {
|
|
22
22
|
return {
|
|
23
23
|
'session.schedule.get': (data) => this.get(record(data)),
|
|
24
|
+
'session.schedule.list': (data) => this.list(record(data)),
|
|
25
|
+
'session.schedule.agent.save': (data) => this.saveFromAgent(record(data)),
|
|
24
26
|
'session.schedule.save': (data) => this.save(record(data)),
|
|
25
27
|
'session.schedule.remove': (data) => this.remove(record(data)),
|
|
26
28
|
'session.schedule.proposal.resolve': (data) => this.resolveProposal(record(data))
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
list(input) {
|
|
32
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
33
|
+
this.options.database().getWorkspace(input.workspaceId) === undefined)
|
|
34
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
35
|
+
if ((input.cursor !== undefined && typeof input.cursor !== 'string') ||
|
|
36
|
+
(input.limit !== undefined && typeof input.limit !== 'number'))
|
|
37
|
+
throw new Error('SCHEDULE_INVALID');
|
|
38
|
+
const page = this.store().list(input.workspaceId, input.cursor, input.limit);
|
|
39
|
+
return {
|
|
40
|
+
...page,
|
|
41
|
+
tasks: page.tasks.map((task) => {
|
|
42
|
+
const session = this.options.runtime.getAgentSession(task.sessionId);
|
|
43
|
+
return {
|
|
44
|
+
...task,
|
|
45
|
+
sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionTitle
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async saveFromAgent(input) {
|
|
51
|
+
const session = await this.requireSession(input, input.mode === 'create');
|
|
52
|
+
const actor = input.__requestUserId;
|
|
53
|
+
if (!Number.isSafeInteger(actor) || actor < 1)
|
|
54
|
+
throw new Error('USER_AUTHORIZATION_REQUIRED');
|
|
55
|
+
const sessionId = session.externalSessionId ?? session.id;
|
|
56
|
+
if (input.mode === 'create') {
|
|
57
|
+
if (this.store().get(sessionId) !== undefined)
|
|
58
|
+
throw new Error('SCHEDULE_ALREADY_EXISTS');
|
|
59
|
+
if (typeof input.objective !== 'string' || typeof input.cron !== 'string')
|
|
60
|
+
throw new Error('SCHEDULE_INVALID');
|
|
61
|
+
return {
|
|
62
|
+
task: await this.createFromMcp({
|
|
63
|
+
sessionId: session.id,
|
|
64
|
+
workspaceId: session.workspaceId,
|
|
65
|
+
actorUserId: actor,
|
|
66
|
+
objective: input.objective,
|
|
67
|
+
cron: input.cron
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (input.mode !== 'update')
|
|
72
|
+
throw new Error('SCHEDULE_INVALID');
|
|
73
|
+
const result = this.store().updateFromAgent(sessionId, input.expectedRevision, actor, input);
|
|
74
|
+
if ('proposalId' in result) {
|
|
75
|
+
this.options.proposed?.(sessionId, result.proposalId);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
this.options.emitSession(this.options.runtime.getAgentSession(session.id) ?? session);
|
|
79
|
+
this.options.changed?.();
|
|
80
|
+
return { task: result };
|
|
81
|
+
}
|
|
29
82
|
async get(input) {
|
|
30
83
|
const session = await this.requireSession(input);
|
|
31
84
|
const store = this.store();
|
|
@@ -128,14 +181,14 @@ export class SessionScheduleService {
|
|
|
128
181
|
this.options.changed?.();
|
|
129
182
|
return { task, proposals: this.store().pendingProposals(task.id) };
|
|
130
183
|
}
|
|
131
|
-
async requireSession(input) {
|
|
184
|
+
async requireSession(input, allowPendingNative = false) {
|
|
132
185
|
if (typeof input.sessionId !== 'string' || typeof input.workspaceId !== 'string')
|
|
133
186
|
throw new Error('SCHEDULE_INVALID');
|
|
134
187
|
const session = this.options.runtime.getAgentSession(input.sessionId) ??
|
|
135
188
|
(await this.options.projection.createMetadataProjection(input.sessionId));
|
|
136
189
|
if (session === undefined ||
|
|
137
190
|
session.workspaceId !== input.workspaceId ||
|
|
138
|
-
session.externalSessionId === null)
|
|
191
|
+
(session.externalSessionId === null && !allowPendingNative))
|
|
139
192
|
throw new Error('SESSION_NOT_FOUND');
|
|
140
193
|
return session;
|
|
141
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.48",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/marctl": "0.10.8",
|
|
41
40
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/
|
|
41
|
+
"@myagentroam/agent": "0.10.48",
|
|
42
|
+
"@myagentroam/marctl": "0.10.10",
|
|
43
|
+
"@myagentroam/protocol": "0.10.48"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|