openvisio-agent 0.3.6 → 0.3.7

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/watch.mjs +16 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Connect your coding agent (Claude Code) to an OpenVisio team — MCP tools + optional autonomy — in one command. No shell scripts.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -158,7 +158,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
158
158
  let busy = false
159
159
  let queued = null // 'full' | 'fast' — a cycle requested while one was running
160
160
  let handle = null
161
- let lastActivityAt = Date.now()
162
161
  // Context lines from the events themselves (the WS payload already carries the
163
162
  // channel + message / task), so the agent acts on THEM directly instead of
164
163
  // hoping poll_inbox re-surfaces the same item. Accumulated across coalesced
@@ -196,7 +195,6 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
196
195
 
197
196
  function onEvent(k, d) {
198
197
  const raw = d && typeof d === 'object' ? d : {}
199
- lastActivityAt = Date.now()
200
198
  if (k === 'task:assigned') {
201
199
  const t = raw.task || {}
202
200
  log('task:assigned ' + (t.id != null ? '#' + t.id + ' “' + (t.title || '') + '”' : ''))
@@ -207,34 +205,36 @@ function loopBackendWs({ wsUrl, apiKey, identifier, claude, mcpConfig, workdir,
207
205
  const msg = raw.message && typeof raw.message === 'object' ? raw.message : {}
208
206
  const text = String(msg.content || msg.body || msg.text || '').replace(/\s+/g, ' ').slice(0, 600)
209
207
  const who = senderName(msg)
210
- log('agent:mention in channel ' + (cid != null ? cid : '?'))
208
+ // Reply IN THE SAME THREAD: parent is the thread root (the message's own id
209
+ // for a top-level mention, or its parent when the mention is itself a reply).
210
+ const mid = msg.id != null ? msg.id : (msg.message_id != null ? msg.message_id : null)
211
+ const parent = msg.parent_id != null ? msg.parent_id : (msg.parentId != null ? msg.parentId : null)
212
+ const threadRoot = parent != null ? parent : mid
213
+ log('agent:mention in channel ' + (cid != null ? cid : '?') + (threadRoot != null ? ' (thread ' + threadRoot + ')' : ''))
211
214
  const ctx = cid != null
212
- ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by ${who}` : ''}: "${text}". Your FIRST action must be mcp__openvisio-team__post_message with channel_id ${cid}, a 1-3 sentence reply, and the agent_identifier + agent_api_key from the AUTH line above. You already have the message here — do NOT poll_inbox to find it. After posting, you MAY poll_inbox once for other .followUps.`
215
+ ? `You were @mentioned in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". Reply with mcp__openvisio-team__post_message as your FIRST action 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.${who ? ` To @mention them back, write their EXACT full name "@${who}" (a mention only links when the name matches exactly — "@${who.split(' ')[0]}" alone will NOT).` : ''} You already have the message here — do NOT poll_inbox to find it.`
213
216
  : undefined
214
217
  void drain('fast', ctx)
218
+ } else if (k === 'error') {
219
+ // Surface the backend's rejection detail instead of a bare "event error".
220
+ log('error event: ' + JSON.stringify(raw).slice(0, 220))
215
221
  } else {
216
222
  log('event ' + k)
217
223
  }
218
224
  }
219
225
 
220
- // Follow-up sweep: thread replies you are part of get NO @mention (no WS event
221
- // fires), so poll the inbox on a slow cadence to catch .followUps — but ONLY
222
- // within a short window after real activity, so an idle agent costs nothing.
223
- const FOLLOWUP_MS = 60_000
224
- const ACTIVE_WINDOW_MS = 5 * 60_000
225
- const sweep = setInterval(() => {
226
- if (busy) return
227
- if (Date.now() - lastActivityAt > ACTIVE_WINDOW_MS) return // idle → don't spend a cycle
228
- void drain('fast', 'Follow-up check: call poll_inbox and reply to any NEW .followUps (thread replies you are part of, even with no @mention) or unanswered .mentions. If there is nothing new, do nothing and stop — do NOT post.')
229
- }, FOLLOWUP_MS)
230
-
226
+ // NO polling the WebSocket is the only trigger (task:assigned / agent:mention).
227
+ // A thread reply that @mentions the agent fires agent:mention and is handled
228
+ // in-thread above; genuinely un-mentioned thread activity has no WS event, so
229
+ // it's a backend concern (dispatch a thread event to participant agents), not a
230
+ // reason to poll.
231
231
  log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
232
232
  handle = connectAgentWs({ wsUrl, apiKey, identifier, onEvent, log })
233
233
 
234
234
  return new Promise(() => {
235
235
  // Run until killed. Tidy up the socket on termination so a restarting
236
236
  // service doesn't leak a half-open connection.
237
- const bye = () => { clearInterval(sweep); try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
237
+ const bye = () => { try { handle && handle.close() } catch { /* noop */ } process.exit(0) }
238
238
  process.on('SIGTERM', bye)
239
239
  process.on('SIGINT', bye)
240
240
  })