@myagentroam/node 0.10.36 → 0.10.38
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 -0
- package/dist/database.js +3 -1
- 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/managed-run-controller.js +23 -7
- package/dist/runner/mar-agent/marctl-skill.js +1 -1
- package/dist/service/continuous-run-coordinator.js +27 -5
- package/dist/service/hosted-conversation-node-service.js +5 -4
- package/dist/service/scheduled-session-coordinator.js +13 -0
- package/dist/service/scheduled-session-store.js +24 -0
- package/dist/service/session-message-service.js +20 -1
- package/dist/service/workspace-queue-workbench-service.js +7 -5
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -1476,6 +1476,7 @@ export class NodeConnector {
|
|
|
1476
1476
|
onConnected: () => {
|
|
1477
1477
|
this.runtimeCredentials.connected();
|
|
1478
1478
|
this.scheduledRunAuthorization.connected();
|
|
1479
|
+
this.scheduledSessions.connected();
|
|
1479
1480
|
this.restartAuthorization.connected();
|
|
1480
1481
|
if (!this.stopped)
|
|
1481
1482
|
void this.restartHandoff.recover();
|
|
@@ -1866,6 +1867,7 @@ export class NodeConnector {
|
|
|
1866
1867
|
this.usageOutbox.recoverInterrupted();
|
|
1867
1868
|
this.database.closeRunningTerminalSessions();
|
|
1868
1869
|
await this.restartHandoff.load();
|
|
1870
|
+
this.scheduledSessions.recoverInterrupted();
|
|
1869
1871
|
this.scheduledSessions.start();
|
|
1870
1872
|
await this.initializeConversationHosting();
|
|
1871
1873
|
this.computerUseEnablement.initialize();
|
package/dist/database.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
|
|
2
2
|
import { NODE_SCHEMA_MIGRATION_V18 } from './migrations/v018.js';
|
|
3
3
|
import { NODE_SCHEMA_MIGRATION_V19 } from './migrations/v019.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V20 } from './migrations/v020.js';
|
|
4
5
|
import { normalizeSessionPrompt } from './util/personal-instructions.js';
|
|
5
6
|
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
@@ -21,7 +22,8 @@ export const NODE_SCHEMA_MIGRATIONS = [
|
|
|
21
22
|
NODE_SCHEMA_MIGRATION_V16,
|
|
22
23
|
NODE_SCHEMA_MIGRATION_V17,
|
|
23
24
|
NODE_SCHEMA_MIGRATION_V18,
|
|
24
|
-
NODE_SCHEMA_MIGRATION_V19
|
|
25
|
+
NODE_SCHEMA_MIGRATION_V19,
|
|
26
|
+
NODE_SCHEMA_MIGRATION_V20
|
|
25
27
|
];
|
|
26
28
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
27
29
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
|
|
6
|
+
import { NODE_SCHEMA_MIGRATION_V17 } from './v017.js';
|
|
7
|
+
import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
|
|
8
|
+
import { NODE_SCHEMA_MIGRATION_V19 } from './v019.js';
|
|
9
|
+
const sql = `CREATE INDEX scheduled_session_occurrences_unfinished
|
|
10
|
+
ON scheduled_session_occurrences(status) WHERE status IN ('CLAIMED','SUBMITTED');`;
|
|
11
|
+
export const NODE_SCHEMA_MIGRATION_V20 = {
|
|
12
|
+
version: 20,
|
|
13
|
+
sql,
|
|
14
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V19.assert(database),
|
|
15
|
+
assert(database) {
|
|
16
|
+
const reference = new DatabaseSync(':memory:');
|
|
17
|
+
try {
|
|
18
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
19
|
+
for (const migration of [
|
|
20
|
+
NODE_SCHEMA_MIGRATION_V14,
|
|
21
|
+
NODE_SCHEMA_MIGRATION_V15,
|
|
22
|
+
NODE_SCHEMA_MIGRATION_V16,
|
|
23
|
+
NODE_SCHEMA_MIGRATION_V17,
|
|
24
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
25
|
+
NODE_SCHEMA_MIGRATION_V19
|
|
26
|
+
])
|
|
27
|
+
reference.exec(migration.sql);
|
|
28
|
+
reference.exec(sql);
|
|
29
|
+
const schema = (db) => db
|
|
30
|
+
.prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
|
|
31
|
+
.all()
|
|
32
|
+
.map((row) => [
|
|
33
|
+
row.type,
|
|
34
|
+
row.name,
|
|
35
|
+
row.name === 'node_schema_migrations'
|
|
36
|
+
? String(row.sql).replace(/\s+/g, '')
|
|
37
|
+
: String(row.sql).replace(/\s+/g, ' ').trim()
|
|
38
|
+
]);
|
|
39
|
+
if (JSON.stringify(schema(reference)) !== JSON.stringify(schema(database)))
|
|
40
|
+
throw new Error('NODE_DATABASE_SCHEMA_INVALID:scheduled_session_occurrences_unfinished');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
reference.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Read-only platform evidence; never inject input or replace clipboard contents. */
|
|
2
|
+
export const PYTHON_PROBE_SOURCE = String.raw `
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
result = dict(supportsScreenshot=False, supportsPrimaryDisplay=False,
|
|
11
|
+
supportsInput=False, supportsUnicodePaste=False,
|
|
12
|
+
reasonCode="COMPUTER_RUNTIME_UNAVAILABLE")
|
|
13
|
+
|
|
14
|
+
def signature(library, name, arguments, returns):
|
|
15
|
+
function = getattr(library, name)
|
|
16
|
+
function.argtypes = arguments
|
|
17
|
+
function.restype = returns
|
|
18
|
+
return function
|
|
19
|
+
|
|
20
|
+
def windows_desktop():
|
|
21
|
+
user = ctypes.WinDLL("user32", use_last_error=True)
|
|
22
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
23
|
+
handle, integer = ctypes.c_void_p, ctypes.c_uint32
|
|
24
|
+
open_desktop = signature(user, "OpenInputDesktop", [integer, ctypes.c_int, integer], handle)
|
|
25
|
+
close_desktop = signature(user, "CloseDesktop", [handle], ctypes.c_int)
|
|
26
|
+
thread_id = signature(kernel, "GetCurrentThreadId", [], integer)
|
|
27
|
+
thread_desktop = signature(user, "GetThreadDesktop", [integer], handle)
|
|
28
|
+
station = signature(user, "GetProcessWindowStation", [], handle)
|
|
29
|
+
information = signature(user, "GetUserObjectInformationW",
|
|
30
|
+
[handle, ctypes.c_int, handle, integer, handle], ctypes.c_int)
|
|
31
|
+
def name(object_handle):
|
|
32
|
+
buffer = ctypes.create_unicode_buffer(256)
|
|
33
|
+
needed = integer()
|
|
34
|
+
if not information(object_handle, 2, buffer, ctypes.sizeof(buffer), ctypes.byref(needed)):
|
|
35
|
+
return None
|
|
36
|
+
return buffer.value
|
|
37
|
+
# DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS. Do not switch or attach desktops.
|
|
38
|
+
desktop = open_desktop(0, False, 0x0001 | 0x0080)
|
|
39
|
+
if not desktop:
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
desktop_name = name(desktop)
|
|
43
|
+
return (name(station()) == "WinSta0" and desktop_name is not None
|
|
44
|
+
and desktop_name == name(thread_desktop(thread_id())))
|
|
45
|
+
finally:
|
|
46
|
+
close_desktop(desktop)
|
|
47
|
+
|
|
48
|
+
def input_supported(gui):
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
return windows_desktop()
|
|
51
|
+
if sys.platform == "darwin":
|
|
52
|
+
services = ctypes.CDLL("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices")
|
|
53
|
+
graphics = ctypes.CDLL("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")
|
|
54
|
+
trusted = signature(services, "AXIsProcessTrusted", [], ctypes.c_bool)
|
|
55
|
+
post_access = signature(graphics, "CGPreflightPostEventAccess", [], ctypes.c_bool)
|
|
56
|
+
return bool(trusted() and post_access())
|
|
57
|
+
if sys.platform == "linux":
|
|
58
|
+
display = gui._pyautogui_x11._display
|
|
59
|
+
if not display.has_extension("XTEST"):
|
|
60
|
+
return False
|
|
61
|
+
display.xtest_get_version(2, 2)
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def clipboard_supported():
|
|
66
|
+
import pyperclip
|
|
67
|
+
copy, paste = pyperclip.determine_clipboard()
|
|
68
|
+
if not callable(copy) or not callable(paste) or not copy or not paste:
|
|
69
|
+
return False
|
|
70
|
+
names = (getattr(copy, "__name__", ""), getattr(paste, "__name__", ""))
|
|
71
|
+
if sys.platform == "win32" and names == ("copy_windows", "paste_windows"):
|
|
72
|
+
user = ctypes.WinDLL("user32", use_last_error=True)
|
|
73
|
+
open_clipboard = signature(user, "OpenClipboard", [ctypes.c_void_p], ctypes.c_int)
|
|
74
|
+
close_clipboard = signature(user, "CloseClipboard", [], ctypes.c_int)
|
|
75
|
+
if not open_clipboard(None):
|
|
76
|
+
return False
|
|
77
|
+
# Opening and closing does not empty, read, or take ownership of the clipboard.
|
|
78
|
+
return bool(close_clipboard())
|
|
79
|
+
if sys.platform == "darwin":
|
|
80
|
+
import AppKit
|
|
81
|
+
board = AppKit.NSPasteboard.generalPasteboard()
|
|
82
|
+
if board is None:
|
|
83
|
+
return False
|
|
84
|
+
# Newer systems expose pasteboard policy. Do not provoke an access prompt.
|
|
85
|
+
if hasattr(board, "accessBehavior"):
|
|
86
|
+
allowed = getattr(AppKit, "NSPasteboardAccessBehaviorAlwaysAllow", None)
|
|
87
|
+
if allowed is None or board.accessBehavior() != allowed:
|
|
88
|
+
return False
|
|
89
|
+
if names == ("copy_osx_pyobjc", "paste_osx_pyobjc"):
|
|
90
|
+
return True
|
|
91
|
+
return (names == ("copy_osx_pbcopy", "paste_osx_pbcopy")
|
|
92
|
+
and shutil.which("pbcopy") is not None and shutil.which("pbpaste") is not None)
|
|
93
|
+
if sys.platform == "linux":
|
|
94
|
+
commands = {("copy_xclip", "paste_xclip"): ["xclip", "-version"],
|
|
95
|
+
("copy_xsel", "paste_xsel"): ["xsel", "--version"]}
|
|
96
|
+
command = commands.get(names)
|
|
97
|
+
executable = shutil.which(command[0]) if command is not None else None
|
|
98
|
+
if executable is None:
|
|
99
|
+
return False
|
|
100
|
+
# X11 access was established above. Verify the selected backend can run,
|
|
101
|
+
# without consuming a one-shot selection or requesting clipboard contents.
|
|
102
|
+
subprocess.run([executable, *command[1:]], stdin=subprocess.DEVNULL,
|
|
103
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True, timeout=2)
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if sys.platform not in ("win32", "darwin", "linux") or (sys.platform == "linux" and
|
|
109
|
+
(not os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))):
|
|
110
|
+
result["reasonCode"] = "COMPUTER_DESKTOP_UNSUPPORTED"
|
|
111
|
+
else:
|
|
112
|
+
import pyautogui
|
|
113
|
+
image = pyautogui.screenshot()
|
|
114
|
+
if image.width > 0 and image.height > 0:
|
|
115
|
+
result["supportsScreenshot"] = result["supportsPrimaryDisplay"] = True
|
|
116
|
+
result["reasonCode"] = "COMPUTER_INPUT_PERMISSION_DENIED"
|
|
117
|
+
if input_supported(pyautogui):
|
|
118
|
+
result["supportsInput"] = True
|
|
119
|
+
result["reasonCode"] = "COMPUTER_RUNTIME_UNAVAILABLE"
|
|
120
|
+
if clipboard_supported():
|
|
121
|
+
result["supportsUnicodePaste"] = True
|
|
122
|
+
result["reasonCode"] = None
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
print(json.dumps(result))
|
|
126
|
+
`;
|
|
@@ -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
|
}
|
|
@@ -20,6 +20,12 @@ import { MarAgentRuntimeModelFactory, sdkModelConfigurations, closeImageCredenti
|
|
|
20
20
|
import { createImageGenerationExtensions } from './image-generation-extensions.js';
|
|
21
21
|
import { createWebToolsExtensions } from './web-tools-extensions.js';
|
|
22
22
|
import { combineExecutionExtensions } from './execution-extensions.js';
|
|
23
|
+
const HOST_CONFIGURATION_CATEGORIES = [
|
|
24
|
+
'cwd',
|
|
25
|
+
'mcpServers',
|
|
26
|
+
'environment',
|
|
27
|
+
'guaranteedCommands'
|
|
28
|
+
];
|
|
23
29
|
export class MarAgentManagedRunController {
|
|
24
30
|
runner;
|
|
25
31
|
host;
|
|
@@ -592,7 +598,7 @@ export class MarAgentManagedRunController {
|
|
|
592
598
|
throw new Error('MAR_AGENT_SESSION_RUNTIME_ACTIVE');
|
|
593
599
|
if (!existing.released &&
|
|
594
600
|
existing.release === undefined &&
|
|
595
|
-
existing.fingerprint === fingerprint) {
|
|
601
|
+
existing.fingerprint.value === fingerprint.value) {
|
|
596
602
|
const configurations = compactRuntimeConfigurations(input.models, input.compact);
|
|
597
603
|
if (existing.modelFingerprint !== modelFingerprint ||
|
|
598
604
|
configurations.some((model) => model.credential?.type === 'RUNTIME_TICKET' ||
|
|
@@ -631,7 +637,7 @@ export class MarAgentManagedRunController {
|
|
|
631
637
|
});
|
|
632
638
|
return existing;
|
|
633
639
|
}
|
|
634
|
-
await this.#releaseRuntime(existing, 'host_configuration_changed');
|
|
640
|
+
await this.#releaseRuntime(existing, 'host_configuration_changed', HOST_CONFIGURATION_CATEGORIES.filter((category) => existing.fingerprint.categories[category] !== fingerprint.categories[category]));
|
|
635
641
|
}
|
|
636
642
|
const creation = this.#createRuntime(input, sessionKey, fingerprint, hostConfiguration);
|
|
637
643
|
for (const key of creationKeys)
|
|
@@ -791,7 +797,7 @@ export class MarAgentManagedRunController {
|
|
|
791
797
|
return;
|
|
792
798
|
this.#scheduleRuntimeRelease(runtime);
|
|
793
799
|
}
|
|
794
|
-
#releaseRuntime(runtime, reason) {
|
|
800
|
+
#releaseRuntime(runtime, reason, changedCategories = []) {
|
|
795
801
|
if (runtime.release !== undefined)
|
|
796
802
|
return runtime.release;
|
|
797
803
|
runtime.release = (async () => {
|
|
@@ -800,6 +806,7 @@ export class MarAgentManagedRunController {
|
|
|
800
806
|
runtimeId: runtime.runtimeId,
|
|
801
807
|
sessionId: runtime.session.id,
|
|
802
808
|
reason,
|
|
809
|
+
...(changedCategories.length === 0 ? {} : { changedCategories }),
|
|
803
810
|
...(runtime.activeRunId === undefined ? {} : { runId: runtime.activeRunId })
|
|
804
811
|
});
|
|
805
812
|
if (runtime.idleTimer !== undefined) {
|
|
@@ -977,16 +984,25 @@ function isExecutionScopedMcp(server) {
|
|
|
977
984
|
(server.transport === 'STDIO' && server.environment?.['MAR_CTL_USER_TOKEN'] !== undefined));
|
|
978
985
|
}
|
|
979
986
|
function sessionHostFingerprint(input, hostConfiguration) {
|
|
980
|
-
|
|
981
|
-
.update(canonicalJson({
|
|
987
|
+
const configuration = {
|
|
982
988
|
cwd: input.cwd,
|
|
983
989
|
mcpServers: input.mcpServers.filter((server) => !isExecutionScopedMcp(server)),
|
|
984
990
|
environment: Object.entries(hostConfiguration.environment)
|
|
985
991
|
.filter((entry) => entry[1] !== undefined)
|
|
986
992
|
.sort(([left], [right]) => left.localeCompare(right)),
|
|
987
993
|
guaranteedCommands: Object.entries(hostConfiguration.guaranteedCommands).sort(([left], [right]) => left.localeCompare(right))
|
|
988
|
-
}
|
|
989
|
-
|
|
994
|
+
};
|
|
995
|
+
const digest = (value) => createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
996
|
+
return {
|
|
997
|
+
value: digest(configuration),
|
|
998
|
+
// Only fixed category names leave this comparison; never log these value fingerprints.
|
|
999
|
+
categories: {
|
|
1000
|
+
cwd: digest(configuration.cwd),
|
|
1001
|
+
mcpServers: digest(configuration.mcpServers),
|
|
1002
|
+
environment: digest(configuration.environment),
|
|
1003
|
+
guaranteedCommands: digest(configuration.guaranteedCommands)
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
990
1006
|
}
|
|
991
1007
|
function sessionModelFingerprint(input) {
|
|
992
1008
|
const compact = marAgentRuntimeCompactConfigurationSchema.safeParse(input.compact);
|
|
@@ -27,7 +27,7 @@ const marctlInstructions = [
|
|
|
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
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.',
|
|
30
|
-
'
|
|
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.',
|
|
31
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.',
|
|
32
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.',
|
|
33
33
|
'## Verify, recover and respect authorization',
|
|
@@ -115,6 +115,23 @@ export class ContinuousRunCoordinator {
|
|
|
115
115
|
throw new Error('SESSION_NOT_FOUND');
|
|
116
116
|
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
117
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
|
+
});
|
|
118
135
|
const executionEnded = active !== undefined &&
|
|
119
136
|
this.options.runtime.currentExecutionId(active.id) === undefined &&
|
|
120
137
|
activeTurn !== undefined &&
|
|
@@ -123,9 +140,11 @@ export class ContinuousRunCoordinator {
|
|
|
123
140
|
activeTurn.status === 'INTERRUPTED');
|
|
124
141
|
const queueVersionBeforeSettle = this.options.runtime.sessionQueue(sessionId).version;
|
|
125
142
|
const startsAcceptedRun = active !== undefined && input.submissionId === active.id;
|
|
126
|
-
if (startsAcceptedRun || !input.queueBefore.paused ||
|
|
143
|
+
if (startsAcceptedRun || !input.queueBefore.paused || intent !== 'QUEUE')
|
|
127
144
|
this.options.runtime.resumeSessionQueue(sessionId);
|
|
128
|
-
if (input.activeRunId !== undefined &&
|
|
145
|
+
if (input.activeRunId !== undefined &&
|
|
146
|
+
active?.id === input.activeRunId &&
|
|
147
|
+
(input.steerTarget === undefined || steering)) {
|
|
129
148
|
if (item !== undefined)
|
|
130
149
|
this.immediateQueueItems.add(item.id);
|
|
131
150
|
if (executionEnded) {
|
|
@@ -157,13 +176,13 @@ export class ContinuousRunCoordinator {
|
|
|
157
176
|
return 'QUEUED';
|
|
158
177
|
}
|
|
159
178
|
if (active === undefined && item !== undefined) {
|
|
160
|
-
if (input.queueBefore.paused &&
|
|
179
|
+
if (input.queueBefore.paused && intent === 'SEND')
|
|
161
180
|
this.options.runtime.prioritizeQueueItem(item.id);
|
|
162
181
|
this.startIdleQueue(sessionId);
|
|
163
182
|
return 'STARTED';
|
|
164
183
|
}
|
|
165
184
|
if (executionEnded && item !== undefined) {
|
|
166
|
-
if (input.queueBefore.paused &&
|
|
185
|
+
if (input.queueBefore.paused && intent === 'SEND') {
|
|
167
186
|
this.options.runtime.prioritizeQueueItem(item.id);
|
|
168
187
|
this.publish(sessionId);
|
|
169
188
|
}
|
|
@@ -211,7 +230,9 @@ export class ContinuousRunCoordinator {
|
|
|
211
230
|
content: this.options.runtime.getQueueItem(itemId)?.content ?? input
|
|
212
231
|
}
|
|
213
232
|
}),
|
|
214
|
-
input: withNonImageAttachmentPrompt(
|
|
233
|
+
input: withNonImageAttachmentPrompt(pending.precedingInput === undefined
|
|
234
|
+
? resources
|
|
235
|
+
: `${pending.precedingInput}\n\n${resources}`, pending.attachmentPaths, pending.attachmentFiles)
|
|
215
236
|
});
|
|
216
237
|
}
|
|
217
238
|
deleteQueueItem(itemId) {
|
|
@@ -252,6 +273,7 @@ export class ContinuousRunCoordinator {
|
|
|
252
273
|
if (current !== undefined && replacement !== undefined)
|
|
253
274
|
this.preparedInputs.set(item.id, {
|
|
254
275
|
...replacement,
|
|
276
|
+
precedingInput: current.input,
|
|
255
277
|
input: `${current.input}\n\n${replacement.input}`
|
|
256
278
|
});
|
|
257
279
|
}
|
|
@@ -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)
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
import { SCHEDULE_RESTART_OUTCOME_UNKNOWN } from './scheduled-session-store.js';
|
|
1
2
|
export class ScheduledSessionCoordinator {
|
|
2
3
|
options;
|
|
3
4
|
#now;
|
|
4
5
|
#timer;
|
|
5
6
|
#running = false;
|
|
6
7
|
#closed = true;
|
|
8
|
+
#recovered = [];
|
|
7
9
|
constructor(options) {
|
|
8
10
|
this.options = options;
|
|
9
11
|
this.#now = options.now ?? Date.now;
|
|
10
12
|
}
|
|
13
|
+
/** Called only at Node startup, before admitting any new scheduled occurrence. */
|
|
14
|
+
recoverInterrupted() {
|
|
15
|
+
for (const recovered of this.options.store().recoverInterrupted())
|
|
16
|
+
this.#recovered.push(recovered);
|
|
17
|
+
}
|
|
18
|
+
connected() {
|
|
19
|
+
// Startup has no control connection yet. Send these best-effort alerts once it is ready.
|
|
20
|
+
for (const { task, occurrenceId } of this.#recovered.splice(0)) {
|
|
21
|
+
this.options.notify(task, occurrenceId, SCHEDULE_RESTART_OUTCOME_UNKNOWN, 'FAILED');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
start() {
|
|
12
25
|
if (!this.#closed)
|
|
13
26
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assertScheduleTimezone, nextScheduledAt, parseScheduledCron } from './scheduled-session-recurrence.js';
|
|
3
|
+
export const SCHEDULE_RESTART_OUTCOME_UNKNOWN = 'SCHEDULE_RESTART_OUTCOME_UNKNOWN';
|
|
3
4
|
export class ScheduledSessionStore {
|
|
4
5
|
database;
|
|
5
6
|
now;
|
|
@@ -176,6 +177,26 @@ export class ScheduledSessionStore {
|
|
|
176
177
|
throw error;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
180
|
+
recoverInterrupted() {
|
|
181
|
+
return this.write(() => {
|
|
182
|
+
// FAILED closes scheduling bookkeeping, not the unknown execution or its side effects.
|
|
183
|
+
const rows = this.database
|
|
184
|
+
.prepare(`UPDATE scheduled_session_occurrences SET status='FAILED',code=?,updated_at=?
|
|
185
|
+
WHERE status IN ('CLAIMED','SUBMITTED') RETURNING id,task_id`)
|
|
186
|
+
.all(SCHEDULE_RESTART_OUTCOME_UNKNOWN, this.now());
|
|
187
|
+
const tasks = new Map();
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
if (tasks.has(row.task_id))
|
|
190
|
+
continue;
|
|
191
|
+
const task = this.getById(row.task_id);
|
|
192
|
+
if (task === undefined)
|
|
193
|
+
throw new Error('SCHEDULE_TASK_NOT_FOUND');
|
|
194
|
+
tasks.set(task.id, task);
|
|
195
|
+
this.pruneTerminal(row.id);
|
|
196
|
+
}
|
|
197
|
+
return rows.map((row) => ({ task: tasks.get(row.task_id), occurrenceId: row.id }));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
179
200
|
finish(occurrenceId, status, code, runId) {
|
|
180
201
|
const result = this.database
|
|
181
202
|
.prepare(`UPDATE scheduled_session_occurrences SET status=?,code=?,run_id=?,updated_at=?
|
|
@@ -183,6 +204,9 @@ export class ScheduledSessionStore {
|
|
|
183
204
|
.run(status, code ?? null, runId ?? null, this.now(), occurrenceId);
|
|
184
205
|
if (result.changes === 0 || status === 'SUBMITTED')
|
|
185
206
|
return;
|
|
207
|
+
this.pruneTerminal(occurrenceId);
|
|
208
|
+
}
|
|
209
|
+
pruneTerminal(occurrenceId) {
|
|
186
210
|
this.database
|
|
187
211
|
.prepare(`DELETE FROM scheduled_session_occurrences
|
|
188
212
|
WHERE task_id=(SELECT task_id FROM scheduled_session_occurrences WHERE id=?)
|
|
@@ -5,7 +5,7 @@ import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
|
5
5
|
import { personalInstructionsForRunner } from '../util/personal-instructions.js';
|
|
6
6
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
7
7
|
import { publicGoalStartText } from './goal-runtime.js';
|
|
8
|
-
import { serializeComposerTag, composerVisibleText } from '@myagentroam/protocol';
|
|
8
|
+
import { serializeComposerTag, composerVisibleText, steerComposerMessage } from '@myagentroam/protocol';
|
|
9
9
|
import { relative } from 'node:path';
|
|
10
10
|
import { prepareComposerResourceInput, composerResourceInputTags, validateComposerSkills } from './composer-message-input.js';
|
|
11
11
|
const INTERNAL_GOAL = Symbol('internalGoal');
|
|
@@ -161,6 +161,19 @@ export class SessionMessageService {
|
|
|
161
161
|
if (internalSchedule && (active !== undefined || queueBeforeCreate.itemIds.length > 0))
|
|
162
162
|
throw new Error('SCHEDULE_SESSION_BUSY');
|
|
163
163
|
const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
|
|
164
|
+
const userSteer = requestedIntent === 'REPLACE_CURRENT' &&
|
|
165
|
+
goalMessage?.activated !== true &&
|
|
166
|
+
!internalGoal &&
|
|
167
|
+
!internalSchedule &&
|
|
168
|
+
!internalRestore;
|
|
169
|
+
const steerTarget = userSteer
|
|
170
|
+
? {
|
|
171
|
+
turnId: active === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: this.options.runtime.conversationTurnForRun(active.id)?.id,
|
|
174
|
+
executionId: active === undefined ? undefined : this.options.runtime.currentExecutionId(active.id)
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
164
177
|
const acceptedMessage = {
|
|
165
178
|
runId,
|
|
166
179
|
sessionId: session.id,
|
|
@@ -245,6 +258,11 @@ export class SessionMessageService {
|
|
|
245
258
|
workspaceId: currentSession.workspaceId,
|
|
246
259
|
runner: currentSession.runner,
|
|
247
260
|
input: runner.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
|
|
261
|
+
...(userSteer
|
|
262
|
+
? {
|
|
263
|
+
prepareSteerInput: () => runner.prepareMessageInput(steerComposerMessage(runnerInput), planActive ? 'plan' : 'default')
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
248
266
|
clientMessageId,
|
|
249
267
|
attachments: runnerImageAttachments,
|
|
250
268
|
attachmentPaths,
|
|
@@ -285,6 +303,7 @@ export class SessionMessageService {
|
|
|
285
303
|
submissionId,
|
|
286
304
|
intent,
|
|
287
305
|
queueBefore: queueBeforeCreate,
|
|
306
|
+
...(steerTarget === undefined ? {} : { steerTarget }),
|
|
288
307
|
...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
|
|
289
308
|
});
|
|
290
309
|
reservation?.commit((directories) => this.options.attachments.adoptSessionFiles(currentSession.id, submissionId, directories, currentSession.workspaceId, attachmentPaths));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { steerComposerMessage } from '@myagentroam/protocol';
|
|
1
2
|
import { isPlainRecord, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
|
|
2
3
|
import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
|
|
3
4
|
export class WorkspaceQueueWorkbenchService {
|
|
@@ -118,12 +119,13 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
118
119
|
const item = this.requireItem(input.queueItemId, input.workspaceId);
|
|
119
120
|
assertQueueItemOwner(item, input.__requestUserId);
|
|
120
121
|
const content = input.content.trim();
|
|
121
|
-
this.options.runtime.updateQueueItem(item.id, content);
|
|
122
122
|
const prepared = this.options.coordinator.pending(item.id);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.options.
|
|
126
|
-
|
|
123
|
+
const runnerInput = prepared === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: this.options.prepareInput(prepared.runner, withNonImageAttachmentPrompt(prepared.steering ? steerComposerMessage(content) : content, prepared.attachmentPaths, prepared.attachmentFiles), prepared.collaborationMode);
|
|
126
|
+
this.options.runtime.updateQueueItem(item.id, content);
|
|
127
|
+
if (runnerInput !== undefined)
|
|
128
|
+
this.options.coordinator.updatePreparedInput(item.id, runnerInput);
|
|
127
129
|
this.options.emit(item.workspaceId, item.sessionId);
|
|
128
130
|
return {
|
|
129
131
|
queue: this.present(item.workspaceId, item.sessionId),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.38",
|
|
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/agent": "0.10.
|
|
40
|
+
"@myagentroam/agent": "0.10.38",
|
|
41
41
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/marctl": "0.10.
|
|
43
|
-
"@myagentroam/protocol": "0.10.
|
|
42
|
+
"@myagentroam/marctl": "0.10.7",
|
|
43
|
+
"@myagentroam/protocol": "0.10.38"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|