openvisio-agent 0.13.0 → 0.14.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/watch.mjs +69 -42
package/README.md CHANGED
@@ -56,7 +56,7 @@ Then `watch --name ada` auto-detects the backend agent and runs a WebSocket loop
56
56
 
57
57
  Runs the **autonomy loop** — the agent replies to @mentions and picks up tickets on its own. It cheaply polls an inbox endpoint (no model spend when idle) and pokes a single warm Claude Code session only when something new arrives.
58
58
 
59
- Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session checks both `poll_inbox` and `get_marching_orders`, covering unanswered mentions, follow-ups, and assigned tasks missed while offline. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
59
+ Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session uses the backend's actual tools (`list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity`) to recover assigned tasks and recent mention activity missed while offline. The same zero-model check runs every five minutes as a safety net; a model starts only when pending work exists.
60
60
 
61
61
  Routine messages, triage, introductions, catch-up checks, and ticket movement use a lightweight model by default. Claude uses Haiku and Codex uses `gpt-5.6-luna`. Requests that clearly require repository work route to the coding model: Sonnet for Claude and `gpt-5.6-sol` for Codex. Override either lane with `--chat-model` and `--model`.
62
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Connect Claude Code, Codex, or OpenCode to an OpenVisio team — MCP tools + optional autonomy — in one command.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -36,7 +36,7 @@ const REPLY_DISCIPLINE = [
36
36
 
37
37
  // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
38
38
  const CHAT_CHARTER = [
39
- 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Your tools are the openvisio-team chat/ticket tools post_message, poll_inbox, comment_ticket, react_message (your runtime may namespace them, e.g. mcp__openvisio-team__post_message or openvisio-team_post_message call whatever names appear in your tool list). You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
39
+ 'YOU ARE a connected agent in an OpenVisio team, running in CHAT-ONLY mode. Use only tools that actually appear in your openvisio-team tool list. Backend MCP commonly provides post_message, react_message, list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity. Some relay runtimes also provide poll_inbox or get_marching_orders. Never call a tool that is absent. You have NO file/Bash/git tools in this mode, so you cannot write code yourself.',
40
40
  'WORK ETHIC — behave like a dependable teammate: never leave a promise dangling. Either ACT now (reply, or file a ticket) or say plainly you can\'t and offer to file a ticket / tag a coding agent who can. Never invent progress. Close the loop every cycle — the human should never have to remind you to circle back.',
41
41
  '',
42
42
  REPLY_DISCIPLINE,
@@ -64,7 +64,7 @@ const COORDINATE = [
64
64
  // A stable "who you are / how you work" charter prepended to every code cycle.
65
65
  const CODE_CHARTER = [
66
66
  'YOU ARE a connected CODING agent in an OpenVisio team, running ON THE USER\'S LAPTOP. You have REAL tools — use them; do NOT claim you lack a capability without checking what you actually hold. Your toolbox:',
67
- ' • openvisio-team chat/ticket tools — read mentions, post messages, comment on tickets, react (post_message / poll_inbox / comment_ticket / react_message; your runtime may namespace them use whatever names appear in your tool list).',
67
+ ' • openvisio-team tools — use the names actually present. Backend MCP provides project/task discovery through list_agents, list_projects, list_tasks, get_ticket, update_ticket, plus post_message/react_message/list_activity. Relay runtimes may additionally expose poll_inbox, get_marching_orders, or comment_ticket.',
68
68
  ' • Read / Grep / Glob / Edit / Write / MultiEdit — inspect AND change code.',
69
69
  ' • Bash — git (branch, commit, push a branch), gh (clone repos, open PRs), run tests/builds.',
70
70
  'YOUR WORKSPACE: your working directory is a WORKSPACE ROOT that holds the org\'s repos as subfolders. Reuse existing clones and the context you already verified. Read repository AGENTS.md instructions before changing code. For any task: locate the relevant repo under the workspace; clone it only when it is genuinely absent, then work inside that subfolder. Never ask the user for a path you can discover yourself.',
@@ -72,7 +72,7 @@ const CODE_CHARTER = [
72
72
  '',
73
73
  'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
74
74
  ' 1. CLOSE THE LOOP in THIS cycle. Never say "I\'ll do X" and stop. If you commit to something, do it NOW — the human must never have to remind you to circle back.',
75
- ' 2. FINISH, then REPORT. Your LAST action every cycle is a status back to the requester: comment_ticket with what you did (branch + PR link + test result) AND a short channel reply that @mentions the person who asked, by their EXACT full name (a mention only links on an exact full-name match).',
75
+ ' 2. FINISH, then REPORT. Your LAST action every cycle is to update/move the ticket with update_ticket when appropriate and post a short channel result with the branch, PR link, and test result. @mention the requester by their EXACT full name.',
76
76
  ' 3. Be honest and specific. Never invent progress. If you are genuinely blocked (missing repo, unclear spec, a failing tool), say exactly what you need in one message — that IS closing the loop.',
77
77
  ' 4. One reply per channel per cycle; answer several nudges together.',
78
78
  '',
@@ -80,7 +80,7 @@ const CODE_CHARTER = [
80
80
  ].join('\n')
81
81
 
82
82
  const CODE_FULL = [
83
- 'THIS CYCLE: call get_marching_orders and poll_inbox to see assigned tickets + mentions, then act on them.',
83
+ 'THIS CYCLE: discover assigned tickets with the tools that actually exist, then act on them. If get_marching_orders/poll_inbox exist, use them. On the backend MCP, identify yourself with list_agents, call list_projects, then list_tasks for each project and keep tasks whose agent_id or nested agent.identifier is yours.',
84
84
  'DO NOT post a promise or pre-work acknowledgement. Start the repository work immediately. Your first task/channel update must contain either a verified result (branch, commit, PR, tests) or a concrete blocker you actually encountered.',
85
85
  'For real code work (an assigned ticket, or a mention asking for changes), run the full flow end-to-end:',
86
86
  ' 1. GET THE CODE: use verified thread/history/recall context first, locate the target repo under your workspace root, and read its AGENTS.md. Reuse an existing clone; clone only if absent. Check `git status` before changing anything and preserve unrelated user work. Update from the remote only when it is safe. Do this yourself; never ask the user for a path you can discover.',
@@ -88,7 +88,7 @@ const CODE_FULL = [
88
88
  ' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
89
89
  ' 4. COMMIT + PUSH YOUR BRANCH: git add -A && git commit -m "…"; then git push -u origin agent/<slug>. Only ever push your own agent/* branch. Never --force, never push to main/master, never merge.',
90
90
  ' 5. RAISE A PR: gh pr create --fill --base <default-branch> --head agent/<slug> (a clear title + a body summarizing the change and how you verified it). Never gh pr merge.',
91
- ' 6. CLOSE THE LOOP: comment_ticket with the summary + PR link/branch, and post a channel reply that @mentions the requester by their exact full name.',
91
+ ' 6. CLOSE THE LOOP: move/update the ticket with update_ticket when appropriate, and post a channel reply with the summary + PR link/branch that @mentions the requester by their exact full name.',
92
92
  'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
93
93
  ].join('\n')
94
94
 
@@ -111,7 +111,7 @@ const INTRO = [
111
111
  ].join('\n')
112
112
  const SWEEP = [
113
113
  'DAILY CATCH-UP — you may have missed items while offline. Prioritize TASKS.',
114
- 'Call get_marching_orders AND poll_inbox, then:',
114
+ 'Use the available task/inbox tools. If get_marching_orders/poll_inbox are absent, use list_agents + list_projects + list_tasks to find tasks assigned to your agent identity, then:',
115
115
  ' 1. For every task assigned to YOU that you have NOT started or acknowledged: acknowledge once (comment_ticket "Catching up — picking this up now"), then do the work end-to-end and report (branch/PR + a short channel note). Skip tasks assigned to other agents.',
116
116
  ' 2. Answer only the @mentions / follow-ups that were directed at YOU and that you have not already answered — at most one reply per channel. Do not reply to threads aimed at someone else.',
117
117
  'If there is genuinely nothing outstanding, STOP silently — do NOT post a "nothing to do" message.',
@@ -517,7 +517,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
517
517
  const canCode = !!workdir
518
518
  // The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
519
519
  // agent_identifier + agent_api_key as arguments. Hand them over up front.
520
- const credNote = `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call (post_message, poll_inbox, react_message, comment_ticket, get_marching_orders, …). These tools may appear in your tool list namespaced (mcp__openvisio-team__post_message on Claude Code, openvisio-team_post_message on opencode) — call whichever names you actually see. The credentials are given to you right here do NOT hunt for them (no Bash/grep/cat/find, no reading memory); just call the tools with these exact values. (Bash/git/gh ARE for your code work this rule is only about not searching for these keys.)`
520
+ const credNote = `AUTH: the openvisio-team tools REQUIRE two arguments on EVERY call — agent_identifier: "${identifier}" and agent_api_key: "${apiKey}". Include BOTH on every openvisio-team tool call. Use ONLY names shown in the current tool list. On backend MCP, discover work with list_agents, list_projects, list_tasks, get_ticket, update_ticket, and list_activity; get_marching_orders, poll_inbox, and comment_ticket may be absent. Tools may be namespaced — call whichever names actually appear. The credentials are given here; do NOT hunt for them. Bash/git/gh ARE for code work; this rule only forbids searching for keys.`
521
521
  // The STATIC charter + creds are the session system prompt (cached, billed once),
522
522
  // NOT re-sent in every cycle's user message — the big token saving.
523
523
  const systemPrompt = (canCode ? CODE_CHARTER : CHAT_CHARTER) + '\n\n' + credNote
@@ -553,6 +553,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
553
553
  let backlogProbeBusy = false
554
554
  let lastTaskSignature = ''
555
555
  let lastTaskTriggeredAt = 0
556
+ let lastInboxSignature = ''
556
557
  let mcpSessionId = ''
557
558
  let mcpRpcId = 0
558
559
 
@@ -577,7 +578,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
577
578
  }
578
579
  const ensureMcpSession = async () => {
579
580
  if (mcpSessionId) return
580
- const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.13.0' } } }, false)
581
+ const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.14.0' } } }, false)
581
582
  if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
582
583
  await mcpPayload(res)
583
584
  mcpSessionId = res.headers.get('mcp-session-id') || ''
@@ -594,7 +595,17 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
594
595
  }
595
596
  const payload = await mcpPayload(res)
596
597
  if (payload.error) throw new Error(`MCP ${name}: ${payload.error.message || 'tool error'}`)
597
- return JSON.stringify(payload.result ?? payload)
598
+ const result = payload.result ?? payload
599
+ if (result?.isError) {
600
+ const detail = result.content?.find?.((c) => c?.type === 'text')?.text || 'tool error'
601
+ throw new Error(`MCP ${name}: ${detail}`)
602
+ }
603
+ return result
604
+ }
605
+ const toolData = (result) => {
606
+ const text = result?.content?.find?.((c) => c?.type === 'text')?.text
607
+ if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
608
+ try { return JSON.parse(text) } catch { return { text } }
598
609
  }
599
610
 
600
611
  // Reconcile everything that may have arrived while disconnected. This spends no
@@ -603,27 +614,53 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
603
614
  if (!mcpUrl || backlogProbeBusy || busy) return
604
615
  backlogProbeBusy = true
605
616
  try {
606
- const orders = await callMcpTool('get_marching_orders')
607
- const inbox = await callMcpTool('poll_inbox')
608
- const normalized = String(orders).replace(/\\"/g, '"').replace(/\s+/g, ' ').trim()
609
- const empty = !normalized || /\b(?:no|zero)\s+(?:open\s+|assigned\s+|pending\s+)?tasks?\b/i.test(normalized) || /"(?:tasks|assigned|marching_orders)"\s*:\s*\[\s*\]/i.test(normalized)
610
- if (empty) lastTaskSignature = ''
611
- else if (/\b(?:task|ticket|title|status|assigned)\b/i.test(normalized)) {
612
- const signature = normalized.slice(0, 1200)
617
+ const agentsData = toolData(await callMcpTool('list_agents'))
618
+ const agents = Array.isArray(agentsData.agents) ? agentsData.agents : []
619
+ const self = agents.find((a) => String(a.identifier || a.slug || '') === identifier)
620
+ if (!self?.id) throw new Error('list_agents did not return this BYO agent')
621
+ const projectsData = toolData(await callMcpTool('list_projects'))
622
+ const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
623
+ const assigned = []
624
+ const mentionActivity = []
625
+ for (const project of projects) {
626
+ const [tasksData, typesData, activityData] = await Promise.all([
627
+ callMcpTool('list_tasks', { project_id: project.id }).then(toolData),
628
+ callMcpTool('list_task_types', { project_id: project.id }).then(toolData),
629
+ callMcpTool('list_activity', { project_id: project.id }).then(toolData),
630
+ ])
631
+ const doneIds = new Set((Array.isArray(typesData.types) ? typesData.types : Array.isArray(typesData.task_types) ? typesData.task_types : Array.isArray(typesData.taskTypes) ? typesData.taskTypes : []).filter((t) => /^(?:done|complete|completed|closed)$/i.test(String(t.name || '').trim())).map((t) => Number(t.id)))
632
+ for (const task of Array.isArray(tasksData.tasks) ? tasksData.tasks : []) {
633
+ const taskAgentId = Number(task.agent_id ?? task.agentId ?? task.agent?.id)
634
+ const taskIdent = String(task.agent?.identifier ?? task.agent?.slug ?? '')
635
+ if (!task.deleted_at && !doneIds.has(Number(task.type_id ?? task.typeId)) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
636
+ assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId })
637
+ }
638
+ }
639
+ const activities = Array.isArray(activityData.activities) ? activityData.activities : Array.isArray(activityData.activity) ? activityData.activity : []
640
+ const mentionNeedles = [self.name, self.identifier, self.slug, identifier].filter(Boolean).map((s) => '@' + String(s).toLowerCase())
641
+ for (const item of activities) {
642
+ const text = JSON.stringify(item)
643
+ const lower = text.toLowerCase()
644
+ if (mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
645
+ }
646
+ }
647
+ if (!assigned.length) lastTaskSignature = ''
648
+ else {
649
+ const signature = JSON.stringify(assigned)
613
650
  const retryDue = Date.now() - lastTaskTriggeredAt > 30 * 60 * 1000
614
651
  if (signature !== lastTaskSignature || retryDue) {
615
652
  lastTaskSignature = signature
616
653
  lastTaskTriggeredAt = Date.now()
617
- log('backlog reconciliation found pending tasks -> coding cycle')
618
- void drain('full', 'Backlog reconciliation found one or more tasks in get_marching_orders. Call get_marching_orders now, select every open task assigned to YOU, and execute the oldest/highest-priority one immediately. Do not merely acknowledge it. Move it to the active column, complete the repository work, verify it, open the PR, move the ticket to done when appropriate, and report evidence. Ignore tasks assigned to anyone else.')
654
+ log('backlog reconciliation found ' + assigned.length + ' assigned task(s) -> coding cycle')
655
+ void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately. Do not call nonexistent get_marching_orders or poll_inbox tools, and do not merely acknowledge. Use get_ticket if more detail is needed, move it to the active column with update_ticket, complete the repository work, verify it, open the PR, move the ticket to done when appropriate, and report evidence.`)
619
656
  }
620
657
  }
621
- const inboxText = String(inbox).replace(/\\"/g, '"').replace(/\s+/g, ' ').trim()
622
- const hasInbox = /"(?:mentions|messages|follow_?ups|items)"\s*:\s*\[\s*\{/i.test(inboxText) || /\b(?:pending|unanswered|new)\s+(?:mention|message|follow.?up)/i.test(inboxText)
623
- if (hasInbox) {
624
- log('backlog reconciliation found inbox activity -> reply cycle')
625
- void drain('fast', 'Backlog reconciliation found pending inbox activity. Call poll_inbox now and handle every unanswered mention or follow-up directed to YOU. Do not respond to other agents\' messages and do not duplicate a reply already present.')
626
- }
658
+ const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
659
+ if (mentionActivity.length && inboxSignature !== lastInboxSignature) {
660
+ lastInboxSignature = inboxSignature
661
+ log('backlog reconciliation found mention activity -> reply cycle')
662
+ void drain('fast', `Recent project activity contains these messages mentioning YOU: ${inboxSignature}. Handle each still-unanswered mention once using the channel/message ids in the activity. Do not call nonexistent poll_inbox or get_marching_orders tools. Skip anything already answered by you.`)
663
+ } else if (!mentionActivity.length) lastInboxSignature = ''
627
664
  } catch (e) {
628
665
  mcpSessionId = ''
629
666
  log('backlog reconciliation failed: ' + (e && e.message ? e.message : e))
@@ -656,9 +693,10 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
656
693
  // Codex can exit successfully after posting only "I'll do it". For coding
657
694
  // cycles, treat that as incomplete and give it one focused recovery turn.
658
695
  // Do not retry a real blocker, which would only create duplicate noise.
659
- if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && result.didMessage && !result.didCode && !/\b(?:blocked|cannot|can't|missing|need access|permission|unclear)\b/i.test(result.outputText || '')) {
660
- log('coding cycle only acknowledged the task; running one completion recovery')
661
- await runCycle('You posted an acknowledgement but performed no repository work. Do not post another acknowledgement. Resume the same assigned task now: inspect the workspace, make the required changes, verify them, commit and push an agent/* branch, open the PR, then post exactly one result update with evidence. If a real blocker appears, report that blocker once.', codeModel)
696
+ const legitimateNoWork = /\b(?:no (?:open |assigned |pending )?tasks?|not assigned to (?:me|you)|assigned to (?:another|someone else)|blocked|cannot|can't|missing|need access|permission|unclear)\b/i.test(result?.outputText || '')
697
+ if (agent === 'codex' && kind === 'full' && result?.subtype === 'ok' && !result.didCode && !legitimateNoWork) {
698
+ log('coding cycle exited without repository action; running one completion recovery')
699
+ await runCycle('You exited the assigned coding task without performing repository work. Do not post another acknowledgement or plan. Resume the same task now: inspect the workspace, make the required changes, verify them, commit and push an agent/* branch, open the PR, then post exactly one result update with evidence. If a real blocker appears, report that blocker once.', codeModel)
662
700
  }
663
701
  } finally {
664
702
  if (heartbeat) clearInterval(heartbeat)
@@ -801,8 +839,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
801
839
  log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
802
840
  handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, onConnect: () => { setTimeout(() => void reconcileBacklog(), 250) }, log })
803
841
 
804
- const DAY_MS = 24 * 60 * 60 * 1000
805
- let introTimer = null, sweepStartTimer = null, sweepTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
842
+ let introTimer = null, taskProbeStartTimer = null, taskProbeTimer = null
806
843
  if (mcpConfig || mcpUrl) {
807
844
  // Workspace ethics: a one-time hello the FIRST time this agent ever connects.
808
845
  const introMarker = join(OV_DIR, 'intro-' + slugify(identifier) + '.done')
@@ -811,17 +848,9 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
811
848
  log('first connection — introducing self to the workspace')
812
849
  introTimer = setTimeout(() => void drain('intro'), 5000) // let the socket subscribe first
813
850
  }
814
- // Catch-up sweep: shortly after startup (covers downtime) + once every day.
815
- // Sooner rather than later: a freshly-added agent whose WS subscription isn't
816
- // live yet still catches pending mentions/tasks via this REST poll_inbox sweep.
817
- if (claimStartupSweep(identifier)) {
818
- sweepStartTimer = setTimeout(() => { log('startup catch-up sweep'); void drain('sweep', SWEEP) }, 12_000)
819
- } else {
820
- log('startup catch-up sweep skipped (ran within the last 6h)')
821
- }
822
- sweepTimer = setInterval(() => { log('daily catch-up sweep'); void drain('sweep', SWEEP) }, DAY_MS)
823
- // Cheap reliability net: no model runs unless the MCP result actually contains
824
- // assigned work. This also catches assignments created while the socket was down.
851
+ // Reconciliation replaces the old model-driven startup/daily sweep. It uses
852
+ // supported MCP tools directly, stays silent when empty, and hands verified
853
+ // coding tasks to the full lane instead of letting Luna announce it cannot code.
825
854
  taskProbeStartTimer = setTimeout(() => void reconcileBacklog(), 3_000)
826
855
  taskProbeTimer = setInterval(() => void reconcileBacklog(), 5 * 60 * 1000)
827
856
  } else {
@@ -833,8 +862,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
833
862
  // service doesn't leak a half-open connection or a dangling interval.
834
863
  const bye = () => {
835
864
  if (introTimer) clearTimeout(introTimer)
836
- if (sweepStartTimer) clearTimeout(sweepStartTimer)
837
- if (sweepTimer) clearInterval(sweepTimer)
838
865
  if (taskProbeStartTimer) clearTimeout(taskProbeStartTimer)
839
866
  if (taskProbeTimer) clearInterval(taskProbeTimer)
840
867
  try { handle && handle.close() } catch { /* noop */ }