openvisio-agent 0.25.3 → 0.25.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.4] — 2026-09-21
4
+
5
+ - Route the backend's `agent:message` events through the same verified recipient, deduplication, queue, model, and delivery path as targeted mentions.
6
+ - Normalize both nested and direct targeted-message envelopes so valid requests cannot stop after the WebSocket event log without starting a cycle.
7
+
3
8
  ## [0.25.3] — 2026-09-21
4
9
 
5
10
  - Normalize structured and compact Markdown MCP list results through one provider-neutral discovery path, so every runtime can resolve projects, agents, boards, tasks, task types, and channels from the same saved setup.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
4
4
  "description": "Connect Claude Code, Codex, or OpenCode to an OpenVisio team \u2014 MCP tools + optional autonomy \u2014 in one command.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/watch.mjs CHANGED
@@ -747,7 +747,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
747
747
  // re-acknowledge the same assignment.
748
748
  const seenTasks = new Set()
749
749
  // Mentions we've already reacted to (by message id) — the backend can re-deliver
750
- // an agent:mention (reconnect replay, dup fan-out), which otherwise makes the
750
+ // an agent:mention/agent:message (reconnect replay, dup fan-out), which otherwise makes the
751
751
  // agent reply to the SAME message twice.
752
752
  const replayPath = join(stateDir, 'watch-' + slug + '-replay.json')
753
753
  let replayState = {}
@@ -1896,12 +1896,27 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
1896
1896
  }
1897
1897
  }
1898
1898
 
1899
+ const conversationEventData = (value) => {
1900
+ const outer = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
1901
+ const data = outer.data && typeof outer.data === 'object' && !Array.isArray(outer.data) ? outer.data : {}
1902
+ const payload = outer.payload && typeof outer.payload === 'object' && !Array.isArray(outer.payload) ? outer.payload : {}
1903
+ const source = { ...data, ...payload, ...outer }
1904
+ const messageCandidate = [outer.message, data.message, payload.message].find((item) => item && typeof item === 'object' && !Array.isArray(item))
1905
+ const directId = source.message_id ?? source.messageId ?? source.id
1906
+ const directContent = source.content ?? source.body ?? source.text ?? (typeof source.message === 'string' ? source.message : null)
1907
+ const message = messageCandidate || (directId != null && directContent != null ? { ...source, content: directContent } : {})
1908
+ const channelId = source.channel_id ?? source.channelId ?? source.channel?.id ?? message.channel_id ?? message.channelId ?? message.channel?.id
1909
+ return { ...source, channel_id: channelId, message }
1910
+ }
1911
+
1899
1912
  const mentionIntake = new Map()
1900
1913
  function onEvent(k, d) {
1901
1914
  if (stopping) return
1902
- if (k !== 'agent:mention' && k !== 'channel:message') return trackOperation(routeEvent(k, d))
1903
- const cid = d?.channel_id ?? d?.channelId
1904
- const message = d?.message || {}
1915
+ const conversationEvent = k === 'agent:mention' || k === 'agent:message' || k === 'channel:message'
1916
+ if (!conversationEvent) return trackOperation(routeEvent(k, d))
1917
+ const eventData = conversationEventData(d)
1918
+ const cid = eventData.channel_id ?? eventData.channelId
1919
+ const message = eventData.message || {}
1905
1920
  const parent = messageParentId(message)
1906
1921
  const messageId = message.message_id ?? message.messageId ?? message.id
1907
1922
  const malformedParent = parent == null && [message.parent_id, message.parentId, message.thread_id, message.threadId, message.reply_to, message.replyTo].some((value) => value != null)
@@ -1951,7 +1966,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
1951
1966
  }
1952
1967
  if (!stopping) {
1953
1968
  if (!irrelevant) log(`mention timing intake=${Math.round(performance.now() - intakeStartedAt)}ms source=${k} channel=${cid} message=${messageId}`)
1954
- return routeEvent('agent:mention', { ...d, _threadRootContent: rootContent })
1969
+ return routeEvent('agent:mention', { ...eventData, _sourceEvent: k, _threadRootContent: rootContent })
1955
1970
  }
1956
1971
  }).catch((error) => { log('mention thread verification failed: ' + (error?.message || error)); return { status: 'retry' } })
1957
1972
  .finally(() => { if (mentionIntake.get(key) === next) mentionIntake.delete(key) })
@@ -2103,9 +2118,9 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
2103
2118
  }
2104
2119
 
2105
2120
  // The WebSocket drives real-time reactions (task:created/updated assigned to us,
2106
- // agent:mention and channel:message). Verified organization subscriptions
2121
+ // agent:mention, agent:message, and channel:message). Verified organization subscriptions
2107
2122
  // recover channel mentions even when the backend omits targeted dispatch.
2108
- // Both event types share recipient, ownership and deduplication guards.
2123
+ // All conversation event types share recipient, ownership and deduplication guards.
2109
2124
  log('up — backend WS watcher on ' + wsUrl + (canCode ? ' [code: ' + workdir + ']' : ''))
2110
2125
  handle = (dependencies.connect || connectAgentWs)({ wsUrl, apiKey, identifier, onEvent,
2111
2126
  onDisconnect: () => {