openvisio-agent 0.15.0 → 0.15.2

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/README.md CHANGED
@@ -58,7 +58,7 @@ Runs the **autonomy loop** — the agent replies to @mentions and picks up ticke
58
58
 
59
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
- Routine messages, triage, introductions, catch-up checks, and ticket movement use a lower-cost model by default. Claude uses Haiku and Codex uses `gpt-5.6-terra`; Luna is intentionally not used because watcher coordination depends on reliable MCP tool use. Requests that 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`.
61
+ Claude uses Haiku for routine coordination and Sonnet for repository work. Codex uses `gpt-5.6-sol` for every cycle, including messages, mentions, triage, board movement, MCP calls, and coding. This intentionally favors reliability and consistent tool use over the cheaper Codex tiers.
62
62
 
63
63
  ```bash
64
64
  openvisio-agent watch --name ada # run in this terminal
package/bin/cli.mjs CHANGED
@@ -59,8 +59,8 @@ connect --backend
59
59
  gpt-5.6-sol; OpenCode uses its configured default.
60
60
  Engineers can also change it live from chat: "@agent /model …".
61
61
  --chat-model <m> coordination model for chat, triage, and board movement.
62
- Defaults: Claude haiku, Codex gpt-5.6-terra.
63
- Coding work stays on --model.
62
+ Claude defaults to haiku. Codex uses gpt-5.6-sol for every
63
+ cycle, including coordination and messaging.
64
64
  --no-service skip the background service — just save config + print the
65
65
  watch commands to run yourself.
66
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
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
@@ -215,11 +215,10 @@ export async function runWatch({ flags }) {
215
215
  const workdir = chatOnly ? '' : (explicitWorkdir || (saved && (saved.workspace || saved.workdir)) || DEFAULT_WORKSPACE)
216
216
  if (workdir) { try { mkdirSync(workdir, { recursive: true }) } catch { /* best-effort; spawn will surface a real problem */ } }
217
217
 
218
- // Keep routine coordination on a balanced tool-capable tier. Luna proved too
219
- // willing to narrate instead of acting in MCP-heavy watcher cycles, so Codex
220
- // defaults to Terra for coordination and reserves Sol for repository work.
218
+ // Codex uses Sol for every watcher cycle. Reliability and consistent MCP/tool
219
+ // behaviour take priority over maintaining a cheaper coordination lane.
221
220
  const defaultModel = agent === 'claude' ? 'sonnet' : agent === 'codex' ? 'gpt-5.6-sol' : ''
222
- const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-terra' : ''
221
+ const defaultChatModel = agent === 'claude' ? 'haiku' : agent === 'codex' ? 'gpt-5.6-sol' : ''
223
222
  const model = String(flags.model || (saved && saved.model) || defaultModel)
224
223
  const chatModel = String(flags['chat-model'] || (saved && saved.chatModel) || defaultChatModel)
225
224
 
@@ -529,10 +528,16 @@ function createCycleRunner({ claude, agent, mcpUrl, mcpHeaders, cfgKey, mcpConfi
529
528
  function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConfig, mcpUrl, workdir, model, chatModel, debug }) {
530
529
  const log = (m) => process.stdout.write('[ws ' + new Date().toISOString() + '] ' + m + '\n')
531
530
  let handle = null
531
+ let statusEnabled = true
532
+ let lastStatusSentAt = 0
532
533
  // Channels the agent is actively working in this cycle — drives the live
533
534
  // agent:status broadcast (thinking → working → typing → done).
534
535
  const statusTargets = new Set()
535
- const emitStatus = (state) => { for (const c of statusTargets) { try { handle && handle.sendStatus(c, state) } catch { /* best-effort */ } } }
536
+ const sendStatus = (channelId, state) => {
537
+ if (!statusEnabled) return
538
+ try { if (handle) { lastStatusSentAt = Date.now(); handle.sendStatus(channelId, state) } } catch { /* best-effort */ }
539
+ }
540
+ const emitStatus = (state) => { for (const c of statusTargets) sendStatus(c, state) }
536
541
  const canCode = !!workdir
537
542
  // The Mastra bridge authenticates per-CALL: every openvisio-team tool needs
538
543
  // agent_identifier + agent_api_key as arguments. Hand them over up front.
@@ -597,7 +602,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
597
602
  }
598
603
  const ensureMcpSession = async () => {
599
604
  if (mcpSessionId) return
600
- const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.15.0' } } }, false)
605
+ const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.15.2' } } }, false)
601
606
  if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
602
607
  await mcpPayload(res)
603
608
  mcpSessionId = res.headers.get('mcp-session-id') || ''
@@ -700,7 +705,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
700
705
  const prompt = (ctx.length ? ctx.join('\n') + '\n\n' : '') + baseFor(kind)
701
706
  // Chat-shaped cycles (mentions/intro) may run on the cheaper chat model; code
702
707
  // work (full/sweep) uses the main model.
703
- const useModel = kind === 'full' ? codeModel : liteModel
708
+ const useModel = agent === 'codex' ? codeModel : kind === 'full' ? codeModel : liteModel
704
709
  log('running ' + kind + ' cycle…' + (ctx.length ? ' (' + ctx.length + ' event' + (ctx.length === 1 ? '' : 's') + ')' : '') + (useModel ? ' [' + useModel + ']' : ''))
705
710
  // Live status: "working" now + a heartbeat so the UI (and its TTL) stays lit
706
711
  // through a long cycle; onTool flips it to "typing" when post_message fires.
@@ -721,7 +726,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
721
726
  }
722
727
  } finally {
723
728
  if (heartbeat) clearInterval(heartbeat)
724
- for (const c of targets) { try { handle && handle.sendStatus(c, 'done') } catch { /* noop */ } statusTargets.delete(c) }
729
+ for (const c of targets) { sendStatus(c, 'done'); statusTargets.delete(c) }
725
730
  busy = false
726
731
  if (queued || pending.length) { const next = queued || 'fast'; queued = null; void drain(next) }
727
732
  }
@@ -838,15 +843,22 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
838
843
  log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
839
844
  // Light up the live status the instant we pick this up (thinking → the cycle
840
845
  // takes it to working → typing → done).
841
- if (cid != null) { statusTargets.add(cid); try { handle && handle.sendStatus(cid, 'thinking') } catch { /* best-effort */ } }
846
+ if (cid != null) { statusTargets.add(cid); sendStatus(cid, 'thinking') }
842
847
  const codingMention = canCode && needsCode(text)
843
848
  const ctx = cid != null
844
849
  ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". This mention is FOR YOU. ${codingMention ? 'This is repository work: complete the coding flow first, then send' : 'Send'} EXACTLY ONE reply with post_message: arguments: channel_id ${cid}${threadRoot != null ? `, parent_id ${threadRoot} (reply IN THAT THREAD, do not start a new top-level message)` : ''}, plus agent_identifier + agent_api_key from the AUTH line above, and a 1-3 sentence reply. Compose the whole answer, then post it ONCE. Do not post a first reply and then a revised version. FIRST read the recent messages in this thread: if you already answered this, or another agent was the one addressed, do NOT post at all. Be sure of your answer before sending.${who ? ` To @mention them back, write their EXACT full name "@${who}". A mention only links when the name matches exactly.` : ''} You ALREADY have the message here. Do not poll_inbox, and after your single reply, STOP.`
845
850
  : undefined
846
851
  void drain(codingMention ? 'full' : 'fast', ctx)
847
852
  } else if (k === 'error') {
848
- // Surface the backend's rejection detail instead of a bare "event error".
849
- log('error event: ' + JSON.stringify(raw).slice(0, 220))
853
+ const detail = raw && (raw.message || raw.error || raw.reason || raw.code || raw.d?.message || raw.d?.error)
854
+ // Older backend stages reject agent_status. Stop its heartbeat after the
855
+ // first immediate rejection instead of producing an error every nine seconds.
856
+ if (statusEnabled && lastStatusSentAt && Date.now() - lastStatusSentAt < 3000) {
857
+ statusEnabled = false
858
+ log('live agent status unsupported by this backend; disabling status heartbeat' + (detail ? ': ' + String(detail).slice(0, 160) : ''))
859
+ } else {
860
+ log('error event: ' + (detail ? String(detail) : JSON.stringify(raw)).slice(0, 220))
861
+ }
850
862
  } else {
851
863
  log('event ' + k)
852
864
  }
package/src/ws.mjs CHANGED
@@ -79,7 +79,9 @@ export function connectAgentWs({ wsUrl, apiKey, identifier, onEvent, onConnect,
79
79
  try { msg = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data)) } catch { return }
80
80
  const kind = msg && msg.k
81
81
  if (!kind || kind === 'keepalive:ack') return // keepalive round-trip, nothing to do
82
- try { onEvent(kind, msg.d) } catch (e) { log('event handler error: ' + (e && e.message ? e.message : e)) }
82
+ // Error metadata often lives on the JSON envelope rather than `d`. Preserve
83
+ // it so callers do not receive an unhelpful empty object.
84
+ try { onEvent(kind, kind === 'error' ? msg : msg.d) } catch (e) { log('event handler error: ' + (e && e.message ? e.message : e)) }
83
85
  })
84
86
 
85
87
  const down = (why) => {