{"record":{"id":"33725f6661c5dd8c","repo":"NousResearch/hermes-agent","slug":"remote-gateway-url-is-not-valid-error-message","errorCode":null,"errorMessage":"Remote gateway URL is not valid: ${error.message}","messagePattern":"Remote gateway URL is not valid: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/connection-config.ts","lineNumber":74,"sourceCode":"    throw new Error('Remote gateway URL is required.')\n  }\n\n  // Users routinely paste scheme-less \"host:port\" (a Tailscale IP, a LAN\n  // hostname). Without this, `new URL('100.64.0.1:9119')` either throws or —\n  // worse — parses `host:` as the protocol and produces a baffling\n  // \"must be http:// or https://, got myhost:\" error. Only a real\n  // `scheme://` prefix opts out, so explicit non-http schemes (ftp://,\n  // file://) still reach the protocol check below and get rejected.\n  if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value)) {\n    value = `http://${value}`\n  }\n\n  let parsed\n\n  try {\n    parsed = new URL(value)\n  } catch (error) {\n    throw new Error(`Remote gateway URL is not valid: ${error.message}`)\n  }\n\n  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n    throw new Error(`Remote gateway URL must be http:// or https://, got ${parsed.protocol}`)\n  }\n\n  parsed.hash = ''\n  parsed.search = ''\n  parsed.pathname = parsed.pathname.replace(/\\/+$/, '')\n\n  return parsed.toString().replace(/\\/+$/, '')\n}\n\nfunction buildGatewayWsUrl(baseUrl, token) {\n  const parsed = new URL(baseUrl)\n  const wsScheme = parsed.protocol === 'https:' ? 'wss' : 'ws'\n  const prefix = parsed.pathname.replace(/\\/+$/, '')\n","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/connection-config.ts#L56-L92","documentation":"ValueError from _validate_execution_mode (cron/jobs.py:1552), the single shared validator called by BOTH create_job and update_job: a job was given both monitor_script and monitor_url. A monitor job watches exactly one source for changes (to suppress or wake the agent), so two sources are ambiguous and rejected up front — the update door cannot reintroduce the violation either.","triggerScenarios":"create_job(monitor_script='...', monitor_url='...') or update_job(id, {monitor_script: '...'}) on a job that already has monitor_url set (or vice versa).","commonSituations":"Iteratively building a monitor job and adding a URL after starting with a script; merging two job configs programmatically; an LLM 'upgrading' a script monitor to a URL monitor without clearing the old field.","solutions":["Keep exactly one monitor source: pass only monitor_script or only monitor_url.","To switch sources, first update the job setting the old field to None, then set the new one.","If you truly need two sources, create two separate monitor jobs."],"exampleFix":"# before\ncreate_job(prompt=..., schedule=\"5m\", monitor_script=\"curl -s x\", monitor_url=\"https://x\")\n\n# after\ncreate_job(prompt=..., schedule=\"5m\", monitor_url=\"https://x\")  # script removed","handlingStrategy":"validation","validationCode":"def valid_monitor_args(monitor_script, monitor_url) -> bool:\n    return not (bool(monitor_script) and bool(monitor_url))  # at most one source","typeGuard":null,"tryCatchPattern":"try:\n    create_job(..., monitor_script=ms, monitor_url=mu)\nexcept ValueError as e:\n    if \"mutually exclusive\" in str(e):\n        create_job(..., monitor_url=mu)  # keep exactly one source, drop the other\n        raise","preventionTips":["One monitor source per job — model 'switch source' as clear-old-then-set-new in a single update.","When merging job configs, assert at most one monitor_* key is present."],"tags":["cron","monitor","validation","valueerror"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}