@myagentroam/node 0.10.35 → 0.10.37
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 +115 -18
- package/dist/database.js +29 -12
- package/dist/main.js +18 -2
- package/dist/migrations/v019.js +9 -0
- package/dist/migrations/v020.js +46 -0
- package/dist/runner/mar-agent/computer-use-probe.js +126 -0
- package/dist/runner/mar-agent/computer-use.js +50 -20
- package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -2
- package/dist/runner/mar-agent-runner.js +32 -0
- package/dist/runtime-state.js +15 -10
- package/dist/service/continuous-run-coordinator.js +58 -5
- package/dist/service/direct-transfer-service.js +58 -11
- package/dist/service/external-session-resume-service.js +2 -1
- package/dist/service/goal-runtime.js +13 -0
- package/dist/service/hosted-conversation-node-service.js +5 -4
- package/dist/service/native-session-projection-service.js +77 -11
- package/dist/service/planned-restart-handoff.js +279 -0
- package/dist/service/restart-session-restorer.js +141 -0
- package/dist/service/{scheduled-run-authorization-control.js → run-authorization-control.js} +17 -10
- package/dist/service/run-event-service.js +2 -0
- package/dist/service/scheduled-session-coordinator.js +13 -0
- package/dist/service/scheduled-session-store.js +24 -0
- package/dist/service/session-catalog-service.js +57 -4
- package/dist/service/session-command-service.js +1 -0
- package/dist/service/session-lifecycle-service.js +25 -8
- package/dist/service/session-message-service.js +80 -15
- package/dist/service/session-presentation-service.js +2 -0
- package/dist/service/session-query-service.js +14 -9
- package/dist/service/workspace-queue-workbench-service.js +15 -8
- package/dist/service/workspace-upload-service.js +128 -29
- package/dist/supervisor.js +7 -3
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@ import { resolve } from 'node:path';
|
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { terminateProcessTree } from '../../process-tree.js';
|
|
8
8
|
import { ComputerUseControlIndicator } from './computer-use-indicator.js';
|
|
9
|
+
import { PYTHON_PROBE_SOURCE } from './computer-use-probe.js';
|
|
9
10
|
const TOOL_NAME = 'mcp__computer__exec';
|
|
10
11
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
11
12
|
const MAX_TIMEOUT_MS = 120_000;
|
|
@@ -545,40 +546,69 @@ export async function probeComputerUse(configuration) {
|
|
|
545
546
|
try {
|
|
546
547
|
await access(configuration.runtimeExecutable);
|
|
547
548
|
const result = await runProbe(configuration.runtimeExecutable);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
available: true,
|
|
552
|
-
reasonCode: null,
|
|
553
|
-
supportsScreenshot: true,
|
|
554
|
-
supportsInput: true,
|
|
555
|
-
supportsUnicodePaste: true,
|
|
556
|
-
supportsPrimaryDisplay: true
|
|
557
|
-
}
|
|
558
|
-
: { ...base, available: false, reasonCode: 'COMPUTER_RUNTIME_UNAVAILABLE' };
|
|
549
|
+
if (result === undefined)
|
|
550
|
+
return { ...base, available: false, reasonCode: 'COMPUTER_RUNTIME_UNAVAILABLE' };
|
|
551
|
+
return { ...base, ...result, available: result.reasonCode === null };
|
|
559
552
|
}
|
|
560
553
|
catch {
|
|
561
554
|
return { ...base, available: false, reasonCode: 'COMPUTER_RUNTIME_UNAVAILABLE' };
|
|
562
555
|
}
|
|
563
556
|
}
|
|
564
557
|
async function runProbe(executable) {
|
|
565
|
-
const child = spawn(executable, [
|
|
566
|
-
'
|
|
567
|
-
|
|
568
|
-
|
|
558
|
+
const child = spawn(executable, ['-c', PYTHON_PROBE_SOURCE], {
|
|
559
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
560
|
+
windowsHide: true
|
|
561
|
+
});
|
|
569
562
|
let stdout = '';
|
|
570
|
-
|
|
563
|
+
let overflow = false;
|
|
564
|
+
child.stdout.on('data', (chunk) => {
|
|
565
|
+
if (stdout.length + chunk.length > 4096)
|
|
566
|
+
overflow = true;
|
|
567
|
+
if (!overflow)
|
|
568
|
+
stdout += chunk.toString('utf8');
|
|
569
|
+
});
|
|
570
|
+
child.stderr.resume();
|
|
571
571
|
return await new Promise((resolveProbe) => {
|
|
572
572
|
const timeout = setTimeout(() => {
|
|
573
|
-
void terminateProcessTree(child).finally(() => resolveProbe(
|
|
573
|
+
void terminateProcessTree(child).finally(() => resolveProbe(undefined));
|
|
574
574
|
}, 10_000);
|
|
575
|
-
child.once('
|
|
575
|
+
child.once('close', (code) => {
|
|
576
576
|
clearTimeout(timeout);
|
|
577
|
-
|
|
577
|
+
if (code !== 0 || overflow)
|
|
578
|
+
return resolveProbe(undefined);
|
|
579
|
+
try {
|
|
580
|
+
const value = JSON.parse(stdout);
|
|
581
|
+
if (value === null ||
|
|
582
|
+
typeof value !== 'object' ||
|
|
583
|
+
typeof value.supportsScreenshot !== 'boolean' ||
|
|
584
|
+
typeof value.supportsInput !== 'boolean' ||
|
|
585
|
+
typeof value.supportsUnicodePaste !== 'boolean' ||
|
|
586
|
+
typeof value.supportsPrimaryDisplay !== 'boolean')
|
|
587
|
+
return resolveProbe(undefined);
|
|
588
|
+
const reasonCode = value.reasonCode === 'COMPUTER_DESKTOP_UNSUPPORTED'
|
|
589
|
+
? value.reasonCode
|
|
590
|
+
: !value.supportsScreenshot || !value.supportsPrimaryDisplay
|
|
591
|
+
? 'COMPUTER_RUNTIME_UNAVAILABLE'
|
|
592
|
+
: !value.supportsInput
|
|
593
|
+
? 'COMPUTER_INPUT_PERMISSION_DENIED'
|
|
594
|
+
: !value.supportsUnicodePaste
|
|
595
|
+
? 'COMPUTER_RUNTIME_UNAVAILABLE'
|
|
596
|
+
: null;
|
|
597
|
+
resolveProbe({
|
|
598
|
+
supportsScreenshot: value.supportsScreenshot,
|
|
599
|
+
supportsInput: value.supportsInput,
|
|
600
|
+
supportsUnicodePaste: value.supportsUnicodePaste,
|
|
601
|
+
supportsPrimaryDisplay: value.supportsPrimaryDisplay,
|
|
602
|
+
reasonCode
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
resolveProbe(undefined);
|
|
607
|
+
}
|
|
578
608
|
});
|
|
579
609
|
child.once('error', () => {
|
|
580
610
|
clearTimeout(timeout);
|
|
581
|
-
resolveProbe(
|
|
611
|
+
resolveProbe(undefined);
|
|
582
612
|
});
|
|
583
613
|
});
|
|
584
614
|
}
|
|
@@ -579,6 +579,34 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
|
|
|
579
579
|
truncated: truncated || source.length > 10_000 || output.length > 10_000
|
|
580
580
|
}, status);
|
|
581
581
|
}
|
|
582
|
+
if (toolName === 'code_exec_wait') {
|
|
583
|
+
const inputRecord = record(input);
|
|
584
|
+
const action = typeof inputRecord?.action === 'string' ? inputRecord.action : 'poll';
|
|
585
|
+
const inputSummary = compactRunnerValue(input, 10_000);
|
|
586
|
+
const result = output === undefined ? null : compactRunnerValue(output, 10_000);
|
|
587
|
+
const resultSummary = [outputSummary, result?.text]
|
|
588
|
+
.filter((value, index, values) => typeof value === 'string' && value.length > 0 && values.indexOf(value) === index)
|
|
589
|
+
.join('\n');
|
|
590
|
+
const details = [
|
|
591
|
+
`Action: ${compactRunnerText(action, 128)}`,
|
|
592
|
+
`Input: ${inputSummary.text || '{}'}`,
|
|
593
|
+
resultSummary.length === 0 ? '' : `Result: ${compactRunnerText(resultSummary, 10_000)}`,
|
|
594
|
+
errorCode === null ? '' : `Error: ${compactRunnerText(errorCode, 80)}`
|
|
595
|
+
]
|
|
596
|
+
.filter(Boolean)
|
|
597
|
+
.join('\n');
|
|
598
|
+
return commandItem({
|
|
599
|
+
itemId: id,
|
|
600
|
+
command: toolName,
|
|
601
|
+
cwd: null,
|
|
602
|
+
outputPreview: details,
|
|
603
|
+
exitCode: null,
|
|
604
|
+
truncated: truncated ||
|
|
605
|
+
inputSummary.truncated ||
|
|
606
|
+
result?.truncated === true ||
|
|
607
|
+
resultSummary.length > 10_000
|
|
608
|
+
}, status);
|
|
609
|
+
}
|
|
582
610
|
const subagent = marAgentSubagentProjection(toolName, input, output, subagentTitle, knownSubagentDetails);
|
|
583
611
|
const toolCategory = isImageGenerationTool(toolName) ? 'IMAGE_GENERATION' : undefined;
|
|
584
612
|
const inputSummary = compactRunnerValue(input, 10_000);
|
|
@@ -18,15 +18,16 @@ export function marctlHostSkill(environment = {}) {
|
|
|
18
18
|
const marctlInstructions = [
|
|
19
19
|
'## MAR Workspace CLI',
|
|
20
20
|
'Use exec to run marctl. Reuse loaded guidance while it remains in context. Consult local marctl help sessions for unclear options and marctl --version when version matters; neither needs a network request. Capabilities and permissions still apply.',
|
|
21
|
-
'Use sessions current --json only when the current identity or scope is unknown. For a known target ID, use sessions show <session-id> --json only when fresh state is needed, without current/list first. Use sessions list --limit 10 --json to discover an unknown target;
|
|
21
|
+
'Use sessions current --json only when the current identity or scope is unknown. For a known target ID, use sessions show <session-id> --json only when fresh state is needed, without current/list first. Use sessions list --limit 10 --json to discover an unknown target; narrow with --query <title> or --parent <session-id>. These query Node-known Sessions without scanning Runners. Use sessions graph <session-id> --json only for a creation tree (bounded to 100 nodes with truncated); start automatically records the current Session as parent, not task ownership. Stop once found and follow nextCursor only if needed. Do not scan the Workspace to prove capacity: check assigned work, recorded worker IDs and overlapping write scopes.',
|
|
22
22
|
'MAR binds sessions to the current Node and Workspace. No --node or --workspace override exists. Use returned native Session IDs, not titles, for later operations. A Workspace terminal may have no current Session. Missing scope, an old Node, expired authorization or an unavailable Runner is an explicit error, not permission to bypass the API.',
|
|
23
23
|
'## Create and coordinate independent Sessions',
|
|
24
|
+
'Change an existing Session with sessions config <session-id> --model <id> --effort high --json; either option can be set alone. Use show for current settings; active execution is not restarted.',
|
|
24
25
|
'Prefer -m / --message for short text; use --message-file for longer UTF-8 text or an existing file (- reads stdin). These options are mutually exclusive. Example: marctl sessions start --runner codex --title "fixer#42 Handle empty totals" -m "Implement issue #42 and report test evidence" --json. Other Runner names are mar-agent, claude-code and opencode. An omitted model uses the target Runner configuration, not the Coordinator model.',
|
|
25
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.',
|
|
26
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.',
|
|
27
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.',
|
|
28
29
|
'Read results with sessions history <session-id> --limit 10 --json (default: latest 10 SEGMENTs, not messages; --limit 1..100). Extract needed assistant_message payload.text from segments[].items[]; keep segment runId and user_message.payload.clientMessageId for queued-input correlation. Reuse the returned page; follow --cursor <nextCursor> only for missing evidence.',
|
|
29
|
-
'
|
|
30
|
+
'When useful, add gentle context to an in-progress task with sessions send <session-id> --steer -m "Additional context: focus on the resume path." --json. 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. 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.',
|
|
30
31
|
'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.',
|
|
31
32
|
'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.',
|
|
32
33
|
'## Verify, recover and respect authorization',
|
|
@@ -369,6 +369,38 @@ export class MarAgentRunner extends AbstractRunner {
|
|
|
369
369
|
await catalog.close().catch(() => undefined);
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
|
+
async restartInputState(sessionId, clientMessageId) {
|
|
373
|
+
const catalog = await openMarAgentSessionCatalog({ homeDirectory: this.homeDirectory });
|
|
374
|
+
try {
|
|
375
|
+
let cursor;
|
|
376
|
+
const states = new Map();
|
|
377
|
+
do {
|
|
378
|
+
const page = await catalog.history(sessionId, {
|
|
379
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
380
|
+
limit: 1000
|
|
381
|
+
});
|
|
382
|
+
for (const entry of page.entries) {
|
|
383
|
+
if (entry.kind !== 'event' || entry.event.turnId === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
const event = entry.event;
|
|
386
|
+
if (event.type === 'execution.completed')
|
|
387
|
+
states.set(event.turnId, event.permissionStop === undefined ? 'completed' : 'paused');
|
|
388
|
+
else if (event.type === 'execution.failed')
|
|
389
|
+
states.set(event.turnId, 'completed');
|
|
390
|
+
else if (event.type === 'execution.cancelled')
|
|
391
|
+
states.set(event.turnId, 'interrupted');
|
|
392
|
+
}
|
|
393
|
+
const user = page.entries.find((entry) => entry.kind === 'user' && entry.clientMessageId === clientMessageId);
|
|
394
|
+
if (user?.kind === 'user')
|
|
395
|
+
return states.get(user.turnId) ?? 'interrupted';
|
|
396
|
+
cursor = page.nextCursor;
|
|
397
|
+
} while (cursor !== undefined);
|
|
398
|
+
return 'missing';
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
await catalog.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
372
404
|
async readOfficialConversation(session, input) {
|
|
373
405
|
if (session.externalSessionId === null)
|
|
374
406
|
return undefined;
|
package/dist/runtime-state.js
CHANGED
|
@@ -84,6 +84,7 @@ export class NodeRuntimeState {
|
|
|
84
84
|
// A first user prompt is only a process-local temporary title. A
|
|
85
85
|
// Runner/native title can replace it on the next discovery refresh.
|
|
86
86
|
titleSource: customTitle === null ? 'AUTO' : 'CUSTOM',
|
|
87
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
87
88
|
lastActivityAt: input.lastActivityAt ?? now,
|
|
88
89
|
createdAt: input.createdAt ?? now
|
|
89
90
|
};
|
|
@@ -163,9 +164,9 @@ export class NodeRuntimeState {
|
|
|
163
164
|
}
|
|
164
165
|
updateAgentSessionConfiguration(input) {
|
|
165
166
|
return this.replaceSession(input.sessionId, {
|
|
166
|
-
model: input.model,
|
|
167
|
-
effort: input.effort,
|
|
168
|
-
access: input.access
|
|
167
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
168
|
+
...(input.effort === undefined ? {} : { effort: input.effort }),
|
|
169
|
+
...(input.access === undefined ? {} : { access: input.access })
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
updateAgentSessionAccess(sessionId, access) {
|
|
@@ -338,6 +339,7 @@ export class NodeRuntimeState {
|
|
|
338
339
|
version: 0,
|
|
339
340
|
clientMessageId: input.clientMessageId,
|
|
340
341
|
...(input.source === undefined ? {} : { source: input.source }),
|
|
342
|
+
...(input.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
341
343
|
...(input.initiatedByUserId === undefined
|
|
342
344
|
? {}
|
|
343
345
|
: { initiatedByUserId: input.initiatedByUserId }),
|
|
@@ -416,6 +418,7 @@ export class NodeRuntimeState {
|
|
|
416
418
|
runner: session.runner,
|
|
417
419
|
clientMessageId: input.clientMessageId,
|
|
418
420
|
content: input.content,
|
|
421
|
+
...(input.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
419
422
|
attachments: input.attachments ?? [],
|
|
420
423
|
...(input.initiatedByUserId === undefined
|
|
421
424
|
? {}
|
|
@@ -449,11 +452,14 @@ export class NodeRuntimeState {
|
|
|
449
452
|
if (itemId === undefined)
|
|
450
453
|
return undefined;
|
|
451
454
|
const item = this.requireQueueItem(itemId);
|
|
452
|
-
const run =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
const run = {
|
|
456
|
+
...active,
|
|
457
|
+
...(active.initiatedByUserId === undefined && item.initiatedByUserId !== undefined
|
|
458
|
+
? { initiatedByUserId: item.initiatedByUserId }
|
|
459
|
+
: {}),
|
|
460
|
+
suppressNotifications: item.suppressNotifications === true
|
|
461
|
+
};
|
|
462
|
+
this.runs.set(run.id, run);
|
|
457
463
|
this.queuedItems.set(sessionId, queue.slice(1));
|
|
458
464
|
this.bumpQueueVersion(sessionId);
|
|
459
465
|
this.queueItems.delete(itemId);
|
|
@@ -482,6 +488,7 @@ export class NodeRuntimeState {
|
|
|
482
488
|
clientMessageId: item.clientMessageId,
|
|
483
489
|
content: content ?? item.content,
|
|
484
490
|
recentUserMessage: item.content,
|
|
491
|
+
...(item.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
485
492
|
...(item.attachments.length === 0 ? {} : { attachments: item.attachments }),
|
|
486
493
|
...(item.initiatedByUserId === undefined ? {} : { initiatedByUserId: item.initiatedByUserId })
|
|
487
494
|
});
|
|
@@ -607,8 +614,6 @@ export class NodeRuntimeState {
|
|
|
607
614
|
return { kind: 'run', run: this.requireRun(submission.id) };
|
|
608
615
|
const item = this.requireQueueItem(submission.id);
|
|
609
616
|
const run = this.activeSessionRun(sessionId);
|
|
610
|
-
if (run === undefined)
|
|
611
|
-
throw new Error('RUN_NOT_FOUND');
|
|
612
617
|
return { kind: 'queue', item, run };
|
|
613
618
|
}
|
|
614
619
|
messagePreparationFailure(sessionId, clientMessageId) {
|
|
@@ -10,9 +10,25 @@ export class ContinuousRunCoordinator {
|
|
|
10
10
|
startedExecutions = new Set();
|
|
11
11
|
scheduledSessions = new Map();
|
|
12
12
|
immediateQueueItems = new Set();
|
|
13
|
+
suspended = false;
|
|
13
14
|
constructor(options) {
|
|
14
15
|
this.options = options;
|
|
15
16
|
}
|
|
17
|
+
suspend() {
|
|
18
|
+
this.suspended = true;
|
|
19
|
+
for (const timer of this.scheduledSessions.values())
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
this.scheduledSessions.clear();
|
|
22
|
+
}
|
|
23
|
+
resumeScheduling() {
|
|
24
|
+
this.suspended = false;
|
|
25
|
+
}
|
|
26
|
+
isImmediate(itemId) {
|
|
27
|
+
return this.immediateQueueItems.has(itemId);
|
|
28
|
+
}
|
|
29
|
+
restoreImmediate(itemId) {
|
|
30
|
+
this.immediateQueueItems.add(itemId);
|
|
31
|
+
}
|
|
16
32
|
accept(message, environment = {}) {
|
|
17
33
|
const release = this.options.retainEnvironment?.(environment);
|
|
18
34
|
let retained = false;
|
|
@@ -31,6 +47,7 @@ export class ContinuousRunCoordinator {
|
|
|
31
47
|
sessionId: message.sessionId,
|
|
32
48
|
clientMessageId: message.clientMessageId,
|
|
33
49
|
content: message.content,
|
|
50
|
+
...(message.suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
34
51
|
...(message.initiatedByUserId === undefined
|
|
35
52
|
? {}
|
|
36
53
|
: { initiatedByUserId: message.initiatedByUserId }),
|
|
@@ -98,6 +115,23 @@ export class ContinuousRunCoordinator {
|
|
|
98
115
|
throw new Error('SESSION_NOT_FOUND');
|
|
99
116
|
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
100
117
|
const activeTurn = active && this.options.runtime.conversationTurnForRun(active.id);
|
|
118
|
+
const steering = input.steerTarget !== undefined &&
|
|
119
|
+
active !== undefined &&
|
|
120
|
+
active.id === input.activeRunId &&
|
|
121
|
+
activeTurn?.id === input.steerTarget.turnId &&
|
|
122
|
+
(activeTurn === undefined
|
|
123
|
+
? !this.isMessageRun(active)
|
|
124
|
+
: this.options.runtime.currentExecutionId(active.id) !== undefined &&
|
|
125
|
+
!['SUCCEEDED', 'FAILED', 'INTERRUPTED'].includes(activeTurn.status)) &&
|
|
126
|
+
(input.steerTarget.executionId === undefined ||
|
|
127
|
+
this.options.runtime.currentExecutionId(active.id) === input.steerTarget.executionId);
|
|
128
|
+
const intent = input.steerTarget !== undefined && !steering ? 'SEND' : input.intent;
|
|
129
|
+
if (steering && prepared?.prepareSteerInput !== undefined && !prepared.steering)
|
|
130
|
+
this.preparedInputs.set(input.submissionId, {
|
|
131
|
+
...prepared,
|
|
132
|
+
input: prepared.prepareSteerInput(),
|
|
133
|
+
steering: true
|
|
134
|
+
});
|
|
101
135
|
const executionEnded = active !== undefined &&
|
|
102
136
|
this.options.runtime.currentExecutionId(active.id) === undefined &&
|
|
103
137
|
activeTurn !== undefined &&
|
|
@@ -106,9 +140,11 @@ export class ContinuousRunCoordinator {
|
|
|
106
140
|
activeTurn.status === 'INTERRUPTED');
|
|
107
141
|
const queueVersionBeforeSettle = this.options.runtime.sessionQueue(sessionId).version;
|
|
108
142
|
const startsAcceptedRun = active !== undefined && input.submissionId === active.id;
|
|
109
|
-
if (startsAcceptedRun || !input.queueBefore.paused ||
|
|
143
|
+
if (startsAcceptedRun || !input.queueBefore.paused || intent !== 'QUEUE')
|
|
110
144
|
this.options.runtime.resumeSessionQueue(sessionId);
|
|
111
|
-
if (input.activeRunId !== undefined &&
|
|
145
|
+
if (input.activeRunId !== undefined &&
|
|
146
|
+
active?.id === input.activeRunId &&
|
|
147
|
+
(input.steerTarget === undefined || steering)) {
|
|
112
148
|
if (item !== undefined)
|
|
113
149
|
this.immediateQueueItems.add(item.id);
|
|
114
150
|
if (executionEnded) {
|
|
@@ -140,13 +176,13 @@ export class ContinuousRunCoordinator {
|
|
|
140
176
|
return 'QUEUED';
|
|
141
177
|
}
|
|
142
178
|
if (active === undefined && item !== undefined) {
|
|
143
|
-
if (input.queueBefore.paused &&
|
|
179
|
+
if (input.queueBefore.paused && intent === 'SEND')
|
|
144
180
|
this.options.runtime.prioritizeQueueItem(item.id);
|
|
145
181
|
this.startIdleQueue(sessionId);
|
|
146
182
|
return 'STARTED';
|
|
147
183
|
}
|
|
148
184
|
if (executionEnded && item !== undefined) {
|
|
149
|
-
if (input.queueBefore.paused &&
|
|
185
|
+
if (input.queueBefore.paused && intent === 'SEND') {
|
|
150
186
|
this.options.runtime.prioritizeQueueItem(item.id);
|
|
151
187
|
this.publish(sessionId);
|
|
152
188
|
}
|
|
@@ -186,7 +222,17 @@ export class ContinuousRunCoordinator {
|
|
|
186
222
|
const resources = prepareComposerResourceInput(input, pending.cwd, pending.workspacePath ?? pending.cwd);
|
|
187
223
|
this.preparedInputs.set(itemId, {
|
|
188
224
|
...pending,
|
|
189
|
-
|
|
225
|
+
...(pending.restartMessage === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: {
|
|
228
|
+
restartMessage: {
|
|
229
|
+
...pending.restartMessage,
|
|
230
|
+
content: this.options.runtime.getQueueItem(itemId)?.content ?? input
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
input: withNonImageAttachmentPrompt(pending.precedingInput === undefined
|
|
234
|
+
? resources
|
|
235
|
+
: `${pending.precedingInput}\n\n${resources}`, pending.attachmentPaths, pending.attachmentFiles)
|
|
190
236
|
});
|
|
191
237
|
}
|
|
192
238
|
deleteQueueItem(itemId) {
|
|
@@ -227,6 +273,7 @@ export class ContinuousRunCoordinator {
|
|
|
227
273
|
if (current !== undefined && replacement !== undefined)
|
|
228
274
|
this.preparedInputs.set(item.id, {
|
|
229
275
|
...replacement,
|
|
276
|
+
precedingInput: current.input,
|
|
230
277
|
input: `${current.input}\n\n${replacement.input}`
|
|
231
278
|
});
|
|
232
279
|
}
|
|
@@ -238,6 +285,8 @@ export class ContinuousRunCoordinator {
|
|
|
238
285
|
this.startIdleQueue(sessionId);
|
|
239
286
|
}
|
|
240
287
|
schedule(sessionId, delayMs = 0) {
|
|
288
|
+
if (this.suspended)
|
|
289
|
+
return;
|
|
241
290
|
if (this.scheduledSessions.has(sessionId))
|
|
242
291
|
return;
|
|
243
292
|
const timer = setTimeout(() => {
|
|
@@ -256,6 +305,8 @@ export class ContinuousRunCoordinator {
|
|
|
256
305
|
this.schedule(sessionId);
|
|
257
306
|
}
|
|
258
307
|
async dispatch(sessionId) {
|
|
308
|
+
if (this.suspended || this.options.stopped())
|
|
309
|
+
return;
|
|
259
310
|
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
260
311
|
if (active === undefined)
|
|
261
312
|
return;
|
|
@@ -323,6 +374,8 @@ export class ContinuousRunCoordinator {
|
|
|
323
374
|
const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
|
|
324
375
|
const scheduledMcp = await this.options.scheduledMcp?.(executionInput);
|
|
325
376
|
const mailMcp = await this.options.mailMcp?.(executionInput);
|
|
377
|
+
if (this.suspended || this.options.stopped())
|
|
378
|
+
return;
|
|
326
379
|
if (!this.goalCurrent(pending)) {
|
|
327
380
|
this.dispatchedExecutions.delete(executionId);
|
|
328
381
|
this.rejectStaleGoal(executionId);
|
|
@@ -77,12 +77,12 @@ export class DirectTransferService {
|
|
|
77
77
|
handleTerminal(value) {
|
|
78
78
|
return this.terminalDirect?.handle(value, (id) => this.transports.get(id)?.peer) ?? false;
|
|
79
79
|
}
|
|
80
|
-
close() {
|
|
80
|
+
close(code = 'DIRECT_NODE_STOPPED') {
|
|
81
81
|
this.terminalDirect?.close();
|
|
82
82
|
for (const transfer of [...this.transfers.values()])
|
|
83
|
-
void this.removeTransfer(transfer,
|
|
83
|
+
void this.removeTransfer(transfer, code);
|
|
84
84
|
for (const transport of [...this.transports.values()])
|
|
85
|
-
this.removeTransport(transport,
|
|
85
|
+
this.removeTransport(transport, code);
|
|
86
86
|
}
|
|
87
87
|
dispose() {
|
|
88
88
|
if (this.disposed)
|
|
@@ -215,6 +215,7 @@ export class DirectTransferService {
|
|
|
215
215
|
uploadCommitted: false,
|
|
216
216
|
opened: false,
|
|
217
217
|
processing: Promise.resolve(),
|
|
218
|
+
controller: new AbortController(),
|
|
218
219
|
lastLeaseAliveAt: this.now(),
|
|
219
220
|
leaseExpiresAt: command.expiresAt,
|
|
220
221
|
leaseTimer: undefined
|
|
@@ -267,10 +268,11 @@ export class DirectTransferService {
|
|
|
267
268
|
channel.close();
|
|
268
269
|
return;
|
|
269
270
|
}
|
|
271
|
+
const pendingTransfer = this.transfers.get(openFrameSessionId(event.data) ?? '');
|
|
270
272
|
void this.openChannel(transport, channel, event.data).catch((error) => {
|
|
271
273
|
channel.close();
|
|
272
274
|
const id = openFrameSessionId(event.data);
|
|
273
|
-
if (id !== undefined)
|
|
275
|
+
if (id !== undefined && this.transfers.get(id) === pendingTransfer)
|
|
274
276
|
this.fail(id, errorCode(error), errorMessage(error));
|
|
275
277
|
});
|
|
276
278
|
};
|
|
@@ -294,16 +296,25 @@ export class DirectTransferService {
|
|
|
294
296
|
transfer.opened = true;
|
|
295
297
|
channel.onmessage = (event) => {
|
|
296
298
|
transfer.processing = transfer.processing
|
|
297
|
-
.then(() =>
|
|
298
|
-
|
|
299
|
+
.then(() => {
|
|
300
|
+
if (this.current(transfer))
|
|
301
|
+
return this.channelMessage(transfer, event.data);
|
|
302
|
+
})
|
|
303
|
+
.catch((error) => {
|
|
304
|
+
if (this.current(transfer))
|
|
305
|
+
this.fail(transfer.command.directSessionId, errorCode(error), errorMessage(error));
|
|
306
|
+
});
|
|
299
307
|
};
|
|
300
308
|
channel.onclose = () => {
|
|
301
309
|
if (this.transfers.get(transfer.command.directSessionId) === transfer)
|
|
302
310
|
void this.removeTransfer(transfer, 'DIRECT_CHANNEL_CLOSED');
|
|
303
311
|
};
|
|
304
|
-
|
|
312
|
+
transfer.processing = this.open(transfer);
|
|
313
|
+
await transfer.processing;
|
|
305
314
|
}
|
|
306
315
|
async channelMessage(transfer, data) {
|
|
316
|
+
if (!this.current(transfer))
|
|
317
|
+
return;
|
|
307
318
|
const descriptor = transfer.command.transfer;
|
|
308
319
|
const expectedSize = transferredSize(descriptor);
|
|
309
320
|
if (descriptor.direction === 'DOWNLOAD') {
|
|
@@ -326,8 +337,10 @@ export class DirectTransferService {
|
|
|
326
337
|
throw new Error('DIRECT_FRAME_INVALID');
|
|
327
338
|
if (control.size !== descriptor.size)
|
|
328
339
|
throw new Error('DIRECT_INTEGRITY_FAILED');
|
|
329
|
-
await transfer.upload.complete(control.size);
|
|
340
|
+
await transfer.upload.complete(control.size, transfer.controller.signal);
|
|
330
341
|
transfer.uploadCommitted = true;
|
|
342
|
+
if (!this.current(transfer))
|
|
343
|
+
return;
|
|
331
344
|
this.emit(transfer.command.directSessionId, {
|
|
332
345
|
type: 'direct.complete',
|
|
333
346
|
directSessionId: transfer.command.directSessionId,
|
|
@@ -340,6 +353,8 @@ export class DirectTransferService {
|
|
|
340
353
|
if (bytes.byteLength === 0 || bytes.byteLength > 256 * 1024 || transfer.upload === undefined)
|
|
341
354
|
throw new Error('DIRECT_CHUNK_INVALID');
|
|
342
355
|
const offset = await transfer.upload.write(bytes);
|
|
356
|
+
if (!this.current(transfer))
|
|
357
|
+
return;
|
|
343
358
|
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset }));
|
|
344
359
|
}
|
|
345
360
|
async open(transfer) {
|
|
@@ -348,6 +363,11 @@ export class DirectTransferService {
|
|
|
348
363
|
if (this.options.openUpload === undefined)
|
|
349
364
|
throw new Error('DIRECT_UPLOAD_UNAVAILABLE');
|
|
350
365
|
transfer.upload = await this.options.openUpload(descriptor);
|
|
366
|
+
if (!this.current(transfer)) {
|
|
367
|
+
if (!transfer.preserveUpload)
|
|
368
|
+
await this.cancelUpload(transfer);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
351
371
|
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset: transfer.upload.offset }));
|
|
352
372
|
this.emit(transfer.command.directSessionId, {
|
|
353
373
|
type: 'direct.opened',
|
|
@@ -359,6 +379,8 @@ export class DirectTransferService {
|
|
|
359
379
|
if (this.options.openDownload === undefined)
|
|
360
380
|
throw new Error('DIRECT_DOWNLOAD_UNAVAILABLE');
|
|
361
381
|
const source = await this.options.openDownload(descriptor);
|
|
382
|
+
if (!this.current(transfer))
|
|
383
|
+
return;
|
|
362
384
|
const expectedSize = transferredSize(descriptor);
|
|
363
385
|
transfer.channel?.send(JSON.stringify({ type: 'ACK', offset: source.offset }));
|
|
364
386
|
this.emit(transfer.command.directSessionId, {
|
|
@@ -444,12 +466,20 @@ export class DirectTransferService {
|
|
|
444
466
|
});
|
|
445
467
|
}
|
|
446
468
|
async removeTransfer(transfer, code) {
|
|
447
|
-
if (!this.
|
|
469
|
+
if (!this.current(transfer))
|
|
448
470
|
return;
|
|
471
|
+
this.transfers.delete(transfer.command.directSessionId);
|
|
449
472
|
clearInterval(transfer.leaseTimer);
|
|
450
|
-
|
|
451
|
-
|
|
473
|
+
const preserveWorkspaceUpload = transfer.command.transfer.direction === 'UPLOAD' &&
|
|
474
|
+
transfer.command.transfer.composerAttachment === undefined &&
|
|
475
|
+
(code === 'DIRECT_CHANNEL_CLOSED' ||
|
|
476
|
+
code === 'DIRECT_CONNECTION_FAILED' ||
|
|
477
|
+
code === 'DIRECT_TRANSFER_CONTROL_UNAVAILABLE');
|
|
478
|
+
transfer.preserveUpload = preserveWorkspaceUpload;
|
|
479
|
+
transfer.controller.abort();
|
|
452
480
|
transfer.channel?.close();
|
|
481
|
+
if (!transfer.uploadCommitted && !preserveWorkspaceUpload)
|
|
482
|
+
await this.cancelUpload(transfer);
|
|
453
483
|
if (code !== undefined)
|
|
454
484
|
this.options.send('direct.error', {
|
|
455
485
|
type: 'direct.error',
|
|
@@ -458,6 +488,23 @@ export class DirectTransferService {
|
|
|
458
488
|
message: directErrorMessage(code)
|
|
459
489
|
});
|
|
460
490
|
}
|
|
491
|
+
current(transfer) {
|
|
492
|
+
return (this.transfers.get(transfer.command.directSessionId) === transfer &&
|
|
493
|
+
!transfer.controller.signal.aborted);
|
|
494
|
+
}
|
|
495
|
+
async cancelUpload(transfer) {
|
|
496
|
+
if (transfer.upload === undefined)
|
|
497
|
+
return;
|
|
498
|
+
transfer.cleanup ??= transfer.upload.cancel().catch(() => {
|
|
499
|
+
this.options.send('direct.error', {
|
|
500
|
+
type: 'direct.error',
|
|
501
|
+
directSessionId: transfer.command.directSessionId,
|
|
502
|
+
code: 'DIRECT_UPLOAD_CLEANUP_FAILED',
|
|
503
|
+
message: 'Temporary upload cleanup could not be completed.'
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
await transfer.cleanup;
|
|
507
|
+
}
|
|
461
508
|
}
|
|
462
509
|
function transferredSize(descriptor) {
|
|
463
510
|
return descriptor.direction === 'DOWNLOAD' && descriptor.range !== undefined
|
|
@@ -70,7 +70,8 @@ export class ExternalSessionResumeService {
|
|
|
70
70
|
access: configuration.access,
|
|
71
71
|
...(session.runnerTitle === null ? {} : { title: session.runnerTitle }),
|
|
72
72
|
...(session.customTitle === null ? {} : { customTitle: session.customTitle }),
|
|
73
|
-
sessionPrompt: session.sessionPrompt ?? null
|
|
73
|
+
sessionPrompt: session.sessionPrompt ?? null,
|
|
74
|
+
parentSessionId: session.parentSessionId ?? null
|
|
74
75
|
});
|
|
75
76
|
},
|
|
76
77
|
workspacePath: this.options.workspacePath,
|
|
@@ -31,6 +31,8 @@ export function stripGoalStartPrompt(text) {
|
|
|
31
31
|
return close === -1 ? '' : text.slice(close + tags.close.length).trimStart();
|
|
32
32
|
}
|
|
33
33
|
export function publicGoalHistoryText(text) {
|
|
34
|
+
if (parseComposerTags(text)[0]?.kind === 'mar_restart')
|
|
35
|
+
return serializeComposerTag('mar_restart', {});
|
|
34
36
|
const tags = goalPromptTags(text);
|
|
35
37
|
if (tags === undefined)
|
|
36
38
|
return text;
|
|
@@ -61,6 +63,17 @@ export class GoalRuntime {
|
|
|
61
63
|
get(sessionId) {
|
|
62
64
|
return this.goals.get(sessionId);
|
|
63
65
|
}
|
|
66
|
+
restartState(sessionId) {
|
|
67
|
+
const goal = this.goals.get(sessionId);
|
|
68
|
+
return goal === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: { goal: { ...goal }, executionFailures: this.executionFailures.get(sessionId) ?? 0 };
|
|
71
|
+
}
|
|
72
|
+
restoreRestartState(state) {
|
|
73
|
+
this.goals.set(state.goal.sessionId, { ...state.goal });
|
|
74
|
+
this.executionFailures.set(state.goal.sessionId, state.executionFailures);
|
|
75
|
+
this.changed(state.goal);
|
|
76
|
+
}
|
|
64
77
|
migrateSession(previousId, nextId) {
|
|
65
78
|
if (previousId === nextId)
|
|
66
79
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { modelUsageGrantSchema } from '@myagentroam/protocol';
|
|
1
|
+
import { modelUsageGrantSchema, steerComposerMessage } from '@myagentroam/protocol';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { constants } from 'node:fs';
|
|
4
4
|
import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
|
|
@@ -446,12 +446,13 @@ export class HostedConversationNodeService {
|
|
|
446
446
|
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
447
447
|
return this.startMessage(payload, input);
|
|
448
448
|
}
|
|
449
|
+
const steerPrompt = steerComposerMessage(input.prompt);
|
|
449
450
|
if (!this.interruptActiveRun(payload.userId, conversation, handle))
|
|
450
451
|
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
451
452
|
await this.runSettlements.get(conversation.id)?.catch(() => undefined);
|
|
452
|
-
return this.startMessage(payload, input);
|
|
453
|
+
return this.startMessage(payload, input, steerPrompt);
|
|
453
454
|
}
|
|
454
|
-
async startMessage(payload, input) {
|
|
455
|
+
async startMessage(payload, input, steerPrompt) {
|
|
455
456
|
let conversation = this.options.store.requireConversation(payload.userId, input.conversationId);
|
|
456
457
|
if (this.cleanupLocks.has(conversation.id))
|
|
457
458
|
throw new Error('HOSTED_CONVERSATION_STATE_INVALID');
|
|
@@ -494,7 +495,7 @@ export class HostedConversationNodeService {
|
|
|
494
495
|
selected = selectModel(version, models, input.modelId ?? conversation.modelId);
|
|
495
496
|
}
|
|
496
497
|
const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.requireAttachment(payload.userId, conversation.id, fileId)));
|
|
497
|
-
const prompt = attachmentPrompt(input.prompt, attachments);
|
|
498
|
+
const prompt = attachmentPrompt(steerPrompt ?? input.prompt, attachments);
|
|
498
499
|
const generatedImagesDirectory = marAgentGeneratedImagesDirectory(paths.homeDirectory, conversation.sessionId);
|
|
499
500
|
const generatedImagesLogicalPath = hostedConversationGeneratedImagesDisplayDirectory(conversation.sessionId);
|
|
500
501
|
if (generatedImagesDirectory === undefined || generatedImagesLogicalPath === undefined)
|