openmausbot 0.1.67 → 0.1.68
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/assets/index-CbaejjQA.js +278 -0
- package/dist/assets/{index-BwpNqgCm.js → index-HqooKFTS.js} +1 -1
- package/dist/assets/index-aJ5EWZOQ.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +2 -0
- package/dist-server/drivers/agents-proxy.js +28 -3
- package/dist-server/enterprise/server/index.js +14587 -0
- package/dist-server/index.js +271 -74
- package/dist-server/openmausbot.js +484 -289
- package/dist-server/pair-cli.js +484 -289
- package/dist-server/server/brand.js +7 -0
- package/dist-server/server/cli.js +23 -1
- package/dist-server/server/config.js +7 -1
- package/dist-server/server/delegations.js +56 -11
- package/dist-server/server/drivers/agents-proxy.js +41 -5
- package/dist-server/server/index.js +194 -60
- package/dist-server/server/routines.js +36 -9
- package/dist-server/server/service-cli.js +63 -0
- package/dist-server/server/service-unit.js +129 -0
- package/dist-server/server/steer-queue.js +2 -1
- package/dist-server/server/store.js +30 -4
- package/dist-server/server/system-prompt.js +1 -0
- package/dist-server/server/turn-dispatch-guard.js +6 -0
- package/dist-server/vps-container-mcp.js +2 -0
- package/enterprise/LICENSE +32 -0
- package/enterprise/README.md +89 -0
- package/enterprise/server/index.js +14587 -0
- package/package.json +2 -1
- package/dist/assets/index-A3CBTxwl.css +0 -1
- package/dist/assets/index-CDuyoVPJ.js +0 -278
|
@@ -10,6 +10,7 @@ import { DATA_DIR } from "./config.js";
|
|
|
10
10
|
import { entitled } from "./enterprise.js";
|
|
11
11
|
const HEX_COLOUR = /^#[0-9a-fA-F]{6}$/;
|
|
12
12
|
const MAX_LOGO_CHARS = 300_000;
|
|
13
|
+
const MAX_FAVICON_CHARS = 120_000;
|
|
13
14
|
export const brandSchema = z
|
|
14
15
|
.object({
|
|
15
16
|
/** Product name shown in the window title, onboarding and settings. */
|
|
@@ -26,6 +27,12 @@ export const brandSchema = z
|
|
|
26
27
|
.optional(),
|
|
27
28
|
/** Where "get help" points for this deployment. */
|
|
28
29
|
supportUrl: z.string().url().startsWith("https://", "supportUrl must be an https:// URL").optional(),
|
|
30
|
+
/** Tab and home-screen icon: an inline data:image/... URI (PNG or SVG, small) or an https:// URL. */
|
|
31
|
+
favicon: z
|
|
32
|
+
.string()
|
|
33
|
+
.max(MAX_FAVICON_CHARS, `favicon must be under ${MAX_FAVICON_CHARS} characters (a 64px PNG or a small SVG)`)
|
|
34
|
+
.refine((v) => v.startsWith("data:image/") || v.startsWith("https://"), "favicon must be a data:image/... URI or an https:// URL")
|
|
35
|
+
.optional(),
|
|
29
36
|
})
|
|
30
37
|
.strict();
|
|
31
38
|
export const DEFAULT_BRAND = { name: "OpenMausBot" };
|
|
@@ -32,12 +32,13 @@ import qrcode from "qrcode-terminal";
|
|
|
32
32
|
import { parseAllowList } from "./account-signin.js";
|
|
33
33
|
import { writeFileAtomic } from "./atomic.js";
|
|
34
34
|
import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
|
|
35
|
+
import { runServiceCommand } from "./service-cli.js";
|
|
35
36
|
import { explainTailscaleFailure, tailscaleServe, tailscaleServeOff, tailscaleStatus } from "./tailscale.js";
|
|
36
37
|
import { defaultSetupIo, SetupCancelled } from "./cli-prompts.js";
|
|
37
38
|
import { normalizePhoneOrigin, phonePairingInstructions, runPhoneSetup } from "./cli-phone-setup.js";
|
|
38
39
|
import { cleanupTunnelOrigin, createTunnelAccount, createTunnelOrigin, describeTunnelAccount, describeTunnelState, ensureCloudflared, FLEET_CREDENTIAL_ENV, fleetAccess, fleetCredential, guardianEntry, startTunnel, tunnelAccess, } from "./tunnel.js";
|
|
39
40
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
40
|
-
const COMMANDS = ["setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "browser", "help", "--help", "-h"];
|
|
41
|
+
const COMMANDS = ["setup", "start", "serve", "pair", "sessions", "status", "login", "logout", "access", "service", "browser", "help", "--help", "-h"];
|
|
41
42
|
export function parseArgs(argv, env = process.env) {
|
|
42
43
|
const implicitStart = !argv.length || (argv[0].startsWith("--") && argv[0] !== "--help");
|
|
43
44
|
const [command = "start", ...rest] = implicitStart ? ["start", ...argv] : argv;
|
|
@@ -105,6 +106,8 @@ export function parseArgs(argv, env = process.env) {
|
|
|
105
106
|
}
|
|
106
107
|
else if (options.command === "access" && arg === "--chat-only")
|
|
107
108
|
options.chatOnly = true;
|
|
109
|
+
else if (options.command === "service" && !options.serviceAction && (arg === "install" || arg === "uninstall"))
|
|
110
|
+
options.serviceAction = arg;
|
|
108
111
|
else if (options.command === "browser" && (arg === "install" || arg === "status"))
|
|
109
112
|
options.browserAction = arg;
|
|
110
113
|
else if (options.command === "browser" && arg === "--with-deps")
|
|
@@ -124,6 +127,8 @@ export function parseArgs(argv, env = process.env) {
|
|
|
124
127
|
return { error: "choose one of --tailscale (your tailnet) and --tunnel (a public address)" };
|
|
125
128
|
if (options.command === "access" && !options.accessAction)
|
|
126
129
|
return { error: "access needs one of: list, add EMAIL [--chat-only], remove EMAIL" };
|
|
130
|
+
if (options.command === "service" && !options.serviceAction)
|
|
131
|
+
return { error: "service needs one of: install [the same options as serve], uninstall" };
|
|
127
132
|
if (options.domain && (options.tailscale || options.tunnel || options.publicUrl))
|
|
128
133
|
return { error: "--domain already gives the server its address; drop --tailscale, --tunnel and --public-url" };
|
|
129
134
|
if (options.local && (options.tailscale || options.tunnel || options.publicUrl))
|
|
@@ -145,6 +150,7 @@ export const USAGE = `openmausbot — your team of AI bots, ready in a few steps
|
|
|
145
150
|
openmausbot login [--email you@example.com]
|
|
146
151
|
openmausbot logout
|
|
147
152
|
openmausbot access list | add EMAIL [--chat-only] | remove EMAIL
|
|
153
|
+
openmausbot service install [--domain HOST | --tunnel | --tailscale] [--port N] [--data-dir DIR] | uninstall
|
|
148
154
|
openmausbot browser install [--with-deps] | status
|
|
149
155
|
|
|
150
156
|
setup choose AI access and optional phone access; keep existing bots and chats
|
|
@@ -159,6 +165,10 @@ logout releases that address and signs out
|
|
|
159
165
|
access who may sign in with an emailed code at /pair: an address or
|
|
160
166
|
@domain; --chat-only gives chat and approvals without settings.
|
|
161
167
|
Takes effect at once, no restart.
|
|
168
|
+
service keep the server running across reboots: writes a systemd unit
|
|
169
|
+
(Linux) or a launchd agent (macOS) for the same serve options and
|
|
170
|
+
prints the commands that install it. Install the package
|
|
171
|
+
permanently first (npm install -g openmausbot).
|
|
162
172
|
browser install: the bots' browser engine (agent-browser, pinned) into the
|
|
163
173
|
data dir, and Chrome for Testing into the user's browser cache.
|
|
164
174
|
--with-deps also installs
|
|
@@ -1013,6 +1023,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1013
1023
|
return runLogin(options);
|
|
1014
1024
|
case "access":
|
|
1015
1025
|
return runAccess(options);
|
|
1026
|
+
case "service":
|
|
1027
|
+
return runServiceCommand({
|
|
1028
|
+
action: options.serviceAction ?? "install",
|
|
1029
|
+
dataDir: options.dataDir,
|
|
1030
|
+
port: options.port,
|
|
1031
|
+
domain: options.domain,
|
|
1032
|
+
tunnel: options.tunnel,
|
|
1033
|
+
tailscale: options.tailscale,
|
|
1034
|
+
label: options.label,
|
|
1035
|
+
script: process.argv[1] ?? "",
|
|
1036
|
+
node: process.execPath,
|
|
1037
|
+
}, { log: (line) => console.log(line), error: (line) => console.error(line) });
|
|
1016
1038
|
case "logout":
|
|
1017
1039
|
return runLogout(options);
|
|
1018
1040
|
case "browser":
|
|
@@ -16,6 +16,8 @@ const BROWSER_PROFILE_ID = /^[a-z0-9_-]{1,40}$/;
|
|
|
16
16
|
export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
|
|
17
17
|
export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
|
|
18
18
|
export const MAX_ROOM_TURN_TIMEOUT_MINUTES = 1_440;
|
|
19
|
+
export const DEFAULT_MAX_CONCURRENT_BOT_THREADS = 3;
|
|
20
|
+
export const MAX_CONCURRENT_BOT_THREADS = 10;
|
|
19
21
|
export const DEFAULT_LOCAL_VM_MODE = "shared";
|
|
20
22
|
export const DEFAULT_LOCAL_VM_MAX_INSTANCES = 2;
|
|
21
23
|
export const MIN_LOCAL_VM_MAX_INSTANCES = 1;
|
|
@@ -253,6 +255,7 @@ const appConfigSchema = z.object({
|
|
|
253
255
|
* system language. Unknown tags degrade to English in the renderer. */
|
|
254
256
|
language: optionalText,
|
|
255
257
|
rooms: roomConfigSchema.optional(),
|
|
258
|
+
threads: z.object({ maxConcurrentPerBot: z.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS) }).strict().optional(),
|
|
256
259
|
localVm: localVmConfigSchema.optional(),
|
|
257
260
|
features: featureConfigSchema.optional(),
|
|
258
261
|
browserProfiles: browserProfilesSchema.optional(),
|
|
@@ -349,6 +352,9 @@ export function vpsSshAlias(cfg) {
|
|
|
349
352
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
350
353
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
351
354
|
}
|
|
355
|
+
export function maxConcurrentBotThreads(cfg) {
|
|
356
|
+
return cfg.threads?.maxConcurrentPerBot ?? DEFAULT_MAX_CONCURRENT_BOT_THREADS;
|
|
357
|
+
}
|
|
352
358
|
export function localVmMode(cfg) {
|
|
353
359
|
return cfg.localVm?.mode ?? DEFAULT_LOCAL_VM_MODE;
|
|
354
360
|
}
|
|
@@ -544,7 +550,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
544
550
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
545
551
|
if (storedProfiles.success)
|
|
546
552
|
disk.browserProfiles = storedProfiles.data;
|
|
547
|
-
for (const key of ["xai", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "localVm", "features"]) {
|
|
553
|
+
for (const key of ["xai", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "localVm", "features"]) {
|
|
548
554
|
const section = checkedPatch[key];
|
|
549
555
|
if (!section)
|
|
550
556
|
continue;
|
|
@@ -251,7 +251,10 @@ export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from
|
|
|
251
251
|
* of the target turn is delegated to `runTarget` so delegations.ts
|
|
252
252
|
* stays free of harness-level concerns (commsDepth is the only thing
|
|
253
253
|
* the caller needs). */
|
|
254
|
-
export function drainDelegations(bus, approvalBus, threadId, runTarget
|
|
254
|
+
export function drainDelegations(bus, approvalBus, threadId, runTarget,
|
|
255
|
+
/** Terminal failures before dispatch also need to wake the source. A
|
|
256
|
+
* launched peer reports through its provider-turn finalizer instead. */
|
|
257
|
+
onSettled) {
|
|
255
258
|
if (drainingThreads.has(threadId)) {
|
|
256
259
|
queuedRedrains.add(threadId);
|
|
257
260
|
return;
|
|
@@ -263,6 +266,10 @@ export function drainDelegations(bus, approvalBus, threadId, runTarget) {
|
|
|
263
266
|
drainingThreads.add(threadId);
|
|
264
267
|
void (async () => {
|
|
265
268
|
for (const item of snapshot) {
|
|
269
|
+
// Stop/deletion may remove queued work while another item awaits a
|
|
270
|
+
// person's approval. A stale snapshot is never authority to launch it.
|
|
271
|
+
if (!pendingDelegations.get(threadId)?.some((candidate) => candidate.id === item.id))
|
|
272
|
+
continue;
|
|
266
273
|
// A shared channel's thread is not owned by any single bot, so each
|
|
267
274
|
// queued item carries its own source bot identity.
|
|
268
275
|
const from = bus.store.botByThread(threadId) ??
|
|
@@ -307,8 +314,19 @@ export function drainDelegations(bus, approvalBus, threadId, runTarget) {
|
|
|
307
314
|
finally {
|
|
308
315
|
// A requeued item (busy target, retries left) stays for the drain
|
|
309
316
|
// that the target's own settling turn will trigger.
|
|
317
|
+
const stillQueued = pendingDelegations.get(threadId)?.some((candidate) => candidate.id === item.id);
|
|
310
318
|
if (outcome !== "requeued")
|
|
311
319
|
acknowledgeDelegation(threadId, item.id);
|
|
320
|
+
if (outcome === "settled" && stillQueued) {
|
|
321
|
+
const receipt = findDelegationReceipt(item.id);
|
|
322
|
+
try {
|
|
323
|
+
if (receipt)
|
|
324
|
+
onSettled?.(receipt);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.error("delegation settled but its source could not be resumed", error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
})().finally(() => {
|
|
@@ -321,7 +339,7 @@ export function drainDelegations(bus, approvalBus, threadId, runTarget) {
|
|
|
321
339
|
const snapshotIds = new Set(snapshot.map((item) => item.id));
|
|
322
340
|
const hasNewItems = pendingDelegations.get(threadId)?.some((item) => !snapshotIds.has(item.id)) ?? false;
|
|
323
341
|
if (redrainRequested || hasNewItems) {
|
|
324
|
-
drainDelegations(bus, approvalBus, threadId, runTarget);
|
|
342
|
+
drainDelegations(bus, approvalBus, threadId, runTarget, onSettled);
|
|
325
343
|
}
|
|
326
344
|
});
|
|
327
345
|
}
|
|
@@ -368,6 +386,19 @@ export function discardDelegations(bus, threadId) {
|
|
|
368
386
|
async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarget) {
|
|
369
387
|
let sender = from;
|
|
370
388
|
let target = bus.store.bot(item.toBotId);
|
|
389
|
+
// Retained approval authorizes the message, not a deleted conversation or
|
|
390
|
+
// revoked room membership. Check every retry before writing to its source.
|
|
391
|
+
if (!sourceThreadBelongsToBot(bus.store, sender.id, sourceThreadId)) {
|
|
392
|
+
recordDelegationReceipt({
|
|
393
|
+
id: item.id,
|
|
394
|
+
sourceThreadId,
|
|
395
|
+
toBotId: item.toBotId,
|
|
396
|
+
toBotName: target?.name ?? item.toBotId,
|
|
397
|
+
status: "dropped",
|
|
398
|
+
result: "the source conversation no longer belongs to the delegating bot",
|
|
399
|
+
});
|
|
400
|
+
return "settled";
|
|
401
|
+
}
|
|
371
402
|
if (!target) {
|
|
372
403
|
recordDelegationReceipt({
|
|
373
404
|
id: item.id,
|
|
@@ -422,6 +453,23 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
422
453
|
}
|
|
423
454
|
if (sender.approvePeerComms && !item.approvalAlreadyGranted) {
|
|
424
455
|
const verdict = await requestPeerApproval(approvalBus, sender, target, item.message, "delegate_bot", sourceThreadId);
|
|
456
|
+
if (!pendingDelegations.get(sourceThreadId)?.some((candidate) => candidate.id === item.id))
|
|
457
|
+
return "settled";
|
|
458
|
+
// Approval may have waited for minutes. Revalidate before even reporting
|
|
459
|
+
// a denial, which otherwise recreates a deleted source transcript.
|
|
460
|
+
const current = bus.store.bot(item.toBotId);
|
|
461
|
+
const currentSender = bus.store.bot(from.id);
|
|
462
|
+
if (!current || !currentSender || !sourceThreadBelongsToBot(bus.store, currentSender.id, sourceThreadId)) {
|
|
463
|
+
recordDelegationReceipt({
|
|
464
|
+
id: item.id,
|
|
465
|
+
sourceThreadId,
|
|
466
|
+
toBotId: item.toBotId,
|
|
467
|
+
toBotName: target.name,
|
|
468
|
+
status: "dropped",
|
|
469
|
+
result: "the peer or source conversation no longer exists",
|
|
470
|
+
});
|
|
471
|
+
return "settled";
|
|
472
|
+
}
|
|
425
473
|
if (verdict !== "allow") {
|
|
426
474
|
recordDelegationReceipt({
|
|
427
475
|
id: item.id,
|
|
@@ -438,14 +486,11 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
438
486
|
});
|
|
439
487
|
return "settled";
|
|
440
488
|
}
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
const currentSender = bus.store.bot(from.id);
|
|
447
|
-
if (!current || !currentSender || !sourceThreadBelongsToBot(bus.store, currentSender.id, sourceThreadId))
|
|
448
|
-
return "settled";
|
|
489
|
+
// A busy-target retry must not ask for the very same approval again.
|
|
490
|
+
item.approvalAlreadyGranted = true;
|
|
491
|
+
savePending();
|
|
492
|
+
// Recheck peer access and busy state too: an approval must not start a
|
|
493
|
+
// second turn or mirror an exchange that cannot actually happen.
|
|
449
494
|
if (dropIfUnreachable(bus, currentSender, current, sourceThreadId, item)) {
|
|
450
495
|
return "settled";
|
|
451
496
|
}
|
|
@@ -490,7 +535,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
490
535
|
const reasonLine = item.reason ? `\n\n[Reason: ${item.reason}]` : "";
|
|
491
536
|
const prefixed = `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
|
|
492
537
|
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id);
|
|
493
|
-
return "
|
|
538
|
+
return "dispatched";
|
|
494
539
|
}
|
|
495
540
|
/** The source thread may be a bot's own task or a shared group where the
|
|
496
541
|
* bot is a member. This is the same check the API gate uses for group turns. */
|
|
@@ -130,6 +130,20 @@ function normalizeScheduleInput(args) {
|
|
|
130
130
|
if (!jsonRecord(raw))
|
|
131
131
|
return { error: `The schedule must be a JSON object. ${SUPPORTED_SCHEDULES}` };
|
|
132
132
|
const type = typeof raw.type === "string" ? raw.type.trim().toLowerCase() : "";
|
|
133
|
+
const fields = type === "once"
|
|
134
|
+
? ["type", "at"]
|
|
135
|
+
: type === "weekly" || type === "daily"
|
|
136
|
+
? ["type", "time", "weekdays"]
|
|
137
|
+
: type === "interval"
|
|
138
|
+
? ["type", "every_minutes", "everyMinutes", "starts_at", "anchorAt"]
|
|
139
|
+
: null;
|
|
140
|
+
// Provider conversions may send unused optional fields as null. Ignore
|
|
141
|
+
// those, but never silently discard an actual scheduling constraint (for
|
|
142
|
+
// example timezone or a misspelled starts_at) and approve different work.
|
|
143
|
+
const unsupported = fields && Object.keys(raw).find((key) => raw[key] != null && !fields.includes(key));
|
|
144
|
+
if (unsupported) {
|
|
145
|
+
return { error: `Unsupported ${type} schedule field "${unsupported}". Weekly and daily times use the computer's timezone from list_routines. ${SUPPORTED_SCHEDULES}` };
|
|
146
|
+
}
|
|
133
147
|
if (type === "once") {
|
|
134
148
|
if (typeof raw.at !== "string" || !raw.at.trim()) {
|
|
135
149
|
return { error: `A once schedule needs "at": a future RFC3339 date-time with an explicit offset, for example 2026-09-01T09:00:00+05:30.` };
|
|
@@ -500,7 +514,29 @@ function routineAction(value) {
|
|
|
500
514
|
}
|
|
501
515
|
function routineFields(args) {
|
|
502
516
|
const fields = {};
|
|
503
|
-
|
|
517
|
+
// list_routines returns the harness names. Accept those when a model
|
|
518
|
+
// copies back a definition, as we already do for interval fields.
|
|
519
|
+
if (args.run_on != null && args.runOn != null && args.run_on !== args.runOn) {
|
|
520
|
+
return { fields, error: "Choose one run_on destination; run_on and runOn disagree." };
|
|
521
|
+
}
|
|
522
|
+
if (args.timeout_minutes != null && args.timeoutMinutes != null && args.timeout_minutes !== args.timeoutMinutes) {
|
|
523
|
+
return { fields, error: "Choose one timeout_minutes limit; timeout_minutes and timeoutMinutes disagree." };
|
|
524
|
+
}
|
|
525
|
+
const runOn = args.run_on ?? args.runOn;
|
|
526
|
+
const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
|
|
527
|
+
if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
|
|
528
|
+
return { fields, error: 'run_on must be "maus" or "cloud".' };
|
|
529
|
+
}
|
|
530
|
+
if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
531
|
+
return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
|
|
532
|
+
}
|
|
533
|
+
if (args.continuity != null && typeof args.continuity !== "boolean") {
|
|
534
|
+
return { fields, error: "continuity must be true or false." };
|
|
535
|
+
}
|
|
536
|
+
if (args.clear_timeout != null && typeof args.clear_timeout !== "boolean") {
|
|
537
|
+
return { fields, error: "clear_timeout must be true or false." };
|
|
538
|
+
}
|
|
539
|
+
if (args.clear_timeout === true && timeoutMinutes != null) {
|
|
504
540
|
return { fields, error: "Choose timeout_minutes or clear_timeout, not both." };
|
|
505
541
|
}
|
|
506
542
|
if (typeof args.name === "string")
|
|
@@ -513,12 +549,12 @@ function routineFields(args) {
|
|
|
513
549
|
return { fields, error: normalized.error };
|
|
514
550
|
fields.schedule = normalized.schedule;
|
|
515
551
|
}
|
|
516
|
-
if (
|
|
517
|
-
fields.runOn =
|
|
552
|
+
if (runOn != null)
|
|
553
|
+
fields.runOn = runOn;
|
|
518
554
|
if (args.clear_timeout === true)
|
|
519
555
|
fields.timeoutMinutes = null;
|
|
520
|
-
else if (
|
|
521
|
-
fields.timeoutMinutes =
|
|
556
|
+
else if (timeoutMinutes != null)
|
|
557
|
+
fields.timeoutMinutes = timeoutMinutes;
|
|
522
558
|
if (typeof args.continuity === "boolean")
|
|
523
559
|
fields.continuity = args.continuity;
|
|
524
560
|
return { fields };
|