openvisio-agent 0.25.2 → 0.25.3
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 +5 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/channel-routing.mjs +2 -1
- package/src/team-data.mjs +41 -0
- package/src/watch.mjs +19 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.3] — 2026-09-21
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Recognize backend-generated dedicated channel names that append the agent identifier suffix, allowing first-connection introductions and fresh threads to reach the correct agent channel.
|
|
7
|
+
|
|
3
8
|
## [0.25.2] — 2026-09-21
|
|
4
9
|
|
|
5
10
|
- Load each provider's available models in Agent Studio, apply the selected model exactly to new ACP sessions, and show the model the running watcher actually uses.
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ The integration follows Mastra's [semantic memory](https://mastra.ai/docs/memory
|
|
|
79
79
|
|
|
80
80
|
Model selections are stable. Exact IDs (including reasoning effort) are preserved. An unsuffixed ACP model may resolve once to the same model with `medium` effort; the watcher saves that exact selection. Unavailable selections produce an explicit error rather than switching generations or families. Claude versioned IDs are preserved, with automatic family fallbacks disabled. Codex uses the configured chat model for replies.
|
|
81
81
|
|
|
82
|
-
Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session discovers and caches the backend's actual `tools/list` response, then uses available tools such as `list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity` to recover assigned tasks and recent mention activity missed while offline. Optional actions such as ticket comments are used only when advertised; their absence cannot strand a completed ticket in a retry loop. The same zero-model check runs every
|
|
82
|
+
Backend/BYO watchers reconcile immediately whenever the process starts or the WebSocket connects. A direct MCP session discovers and caches the backend's actual `tools/list` response, then uses available tools such as `list_agents`, `list_projects`, `list_tasks`, `list_task_types`, and `list_activity` to recover assigned tasks and recent mention activity missed while offline. Team discovery is shared by every runtime provider: structured MCP results and compact Markdown list envelopes are normalized once before the watcher resolves its project, identity, boards, tickets, or dedicated channel. Adding another model provider therefore reuses the same saved team connection and setup path instead of adding provider-specific project discovery. Optional actions such as ticket comments are used only when advertised; their absence cannot strand a completed ticket in a retry loop. The same zero-model check runs every 30 seconds as a safety net; a model starts only when pending work exists.
|
|
83
83
|
|
|
84
84
|
The backend MCP may be stateful or stateless. A successful initialize response without `Mcp-Session-Id` is accepted as stateless, so OpenCode agents do not stop with “MCP initialize returned no session id.” Each OpenCode lane keeps its MCP identity in a private per-agent config directory while the repository is supplied separately with `--dir`; stale workspace configuration therefore cannot swap one agent's credentials for another's. The generated remote configuration sends the agent headers directly, disables OAuth probing, and backend cycles never request relay-only inbox calls or MCP resource-discovery tools in place of team actions.
|
|
85
85
|
|
package/package.json
CHANGED
package/src/channel-routing.mjs
CHANGED
|
@@ -22,9 +22,10 @@ export function dedicatedChannel(channels, { id, identifier, aliases = [], chann
|
|
|
22
22
|
return identifier && ownerIdentifier(channel) === identifier
|
|
23
23
|
})
|
|
24
24
|
if (owned.length) return owned.length === 1 ? owned[0] : null
|
|
25
|
+
const identifierSuffix = String(identifier || '').split('-').at(-1)?.toLowerCase() || ''
|
|
25
26
|
const names = new Set([identifier, ...aliases].filter(Boolean).flatMap((alias) => {
|
|
26
27
|
const name = normalize(alias)
|
|
27
|
-
return [name, `agent-${name}`, `${name}-agent`]
|
|
28
|
+
return [name, `agent-${name}`, `${name}-agent`, ...(identifierSuffix ? [`${name}-${identifierSuffix}`] : [])]
|
|
28
29
|
}).filter((name) => !['general', 'team', 'project', 'dev', 'development'].includes(name)))
|
|
29
30
|
// Explicit ownership always outranks a similar name. A recycled agent name
|
|
30
31
|
// must never route a new thread into somebody else's dedicated channel.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const COLLECTIONS = {
|
|
2
|
+
list_agents: ['agents', 'name'],
|
|
3
|
+
list_boards: ['boards', 'name'],
|
|
4
|
+
list_channels: ['channels', 'name'],
|
|
5
|
+
list_projects: ['projects', 'name'],
|
|
6
|
+
list_tasks: ['tasks', 'title'],
|
|
7
|
+
list_task_types: ['types', 'name'],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function markdownRows(markdown, labelKey) {
|
|
11
|
+
if (typeof markdown !== 'string') return []
|
|
12
|
+
const rows = []
|
|
13
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
14
|
+
const match = /^\s*-\s+`(#?\d+)`(?:\s+(.+?))?\s*$/.exec(line)
|
|
15
|
+
if (!match) continue
|
|
16
|
+
const id = Number(match[1].replace(/^#/, ''))
|
|
17
|
+
if (!Number.isSafeInteger(id) || id <= 0) continue
|
|
18
|
+
rows.push({ id, [labelKey]: String(match[2] || '').trim() })
|
|
19
|
+
}
|
|
20
|
+
return rows
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCollection(data, toolName) {
|
|
24
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return data
|
|
25
|
+
const collection = COLLECTIONS[toolName]
|
|
26
|
+
if (!collection) return data
|
|
27
|
+
const [key, labelKey] = collection
|
|
28
|
+
if (Array.isArray(data[key])) return data
|
|
29
|
+
const rows = markdownRows(data.markdown, labelKey)
|
|
30
|
+
return { ...data, [key]: rows }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Team tools may return either structured arrays or a compact JSON envelope
|
|
34
|
+
// containing Markdown rows. Normalize that transport difference here so every
|
|
35
|
+
// runtime provider uses the same watcher discovery path.
|
|
36
|
+
export function toolData(result, toolName = '') {
|
|
37
|
+
const text = result?.content?.find?.((item) => item?.type === 'text')?.text
|
|
38
|
+
if (typeof text !== 'string') return normalizeCollection(result?.structuredContent ?? result ?? {}, toolName)
|
|
39
|
+
try { return normalizeCollection(JSON.parse(text), toolName) }
|
|
40
|
+
catch { return normalizeCollection(result?.structuredContent ?? { text }, toolName) }
|
|
41
|
+
}
|
package/src/watch.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { claudeFallbackModels, resolveClaudeModel } from './model-selection.mjs'
|
|
|
36
36
|
import { createAgentJournal } from './agent-journal.mjs'
|
|
37
37
|
import { readModelSettings } from './model-settings.mjs'
|
|
38
38
|
import { startStudioInBackground } from './studio-cli.mjs'
|
|
39
|
+
import { toolData } from './team-data.mjs'
|
|
39
40
|
|
|
40
41
|
function runtimeJournal(options, supplied) {
|
|
41
42
|
let journal
|
|
@@ -900,7 +901,11 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
900
901
|
// MCP server returns list_agents as human-readable markdown without identifiers.
|
|
901
902
|
const selfAliases = new Set([slug, name, identifier].map((value) => String(value || '').trim().toLowerCase()).filter(Boolean))
|
|
902
903
|
const rememberSelfAgent = (self) => {
|
|
903
|
-
if (!self || typeof self !== 'object'
|
|
904
|
+
if (!self || typeof self !== 'object') return
|
|
905
|
+
const declaredIdentifier = String(self.identifier || self.slug || '')
|
|
906
|
+
const matchesIdentifier = declaredIdentifier === identifier
|
|
907
|
+
const matchesSavedName = !declaredIdentifier && String(name || '').trim() && String(self.name || '').trim().toLowerCase() === String(name).trim().toLowerCase()
|
|
908
|
+
if (!matchesIdentifier && !matchesSavedName) return
|
|
904
909
|
if (validBackendId(self.id)) selfAgentId = Number(self.id)
|
|
905
910
|
for (const alias of [self.name, self.identifier, self.slug]) if (alias) selfAliases.add(String(alias).toLowerCase())
|
|
906
911
|
identityContext = agentProfileContext(self, identifier)
|
|
@@ -933,8 +938,11 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
933
938
|
const ensureSelfAgent = async () => {
|
|
934
939
|
if (selfAgentId != null) return
|
|
935
940
|
if (!selfRead && Date.now() < selfReadAfter) return
|
|
936
|
-
if (!selfRead) selfRead = callMcpTool('list_agents').then(toolData).then((data) => {
|
|
937
|
-
const
|
|
941
|
+
if (!selfRead) selfRead = callMcpTool('list_agents').then((result) => toolData(result, 'list_agents')).then((data) => {
|
|
942
|
+
const agents = Array.isArray(data.agents) ? data.agents : []
|
|
943
|
+
const identified = agents.find((row) => String(row?.identifier || row?.slug || '') === identifier)
|
|
944
|
+
const named = agents.filter((row) => !row?.identifier && !row?.slug && String(row?.name || '').trim().toLowerCase() === String(name || '').trim().toLowerCase())
|
|
945
|
+
const self = identified || (named.length === 1 ? named[0] : null)
|
|
938
946
|
rememberSelfAgent(self)
|
|
939
947
|
}).catch((error) => {
|
|
940
948
|
log('Agent profile unavailable; continuing with task ownership and configured aliases: ' + (error?.message || error))
|
|
@@ -989,13 +997,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
989
997
|
}
|
|
990
998
|
}
|
|
991
999
|
if (mcpUrl) void discoverMcpTools().catch(() => {})
|
|
992
|
-
const
|
|
993
|
-
const text = result?.content?.find?.((c) => c?.type === 'text')?.text
|
|
994
|
-
if (typeof text !== 'string') return result?.structuredContent ?? result ?? {}
|
|
995
|
-
try { return JSON.parse(text) } catch { return { text } }
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const taskTypeSets = createTaskTypeLookup({ load: (projectId) => loadProjectTaskTypes({ projectId, listTools: () => mcpClient.listTools(), callTool: (name, args) => callMcpTool(name, args).then(toolData) }) })
|
|
1000
|
+
const taskTypeSets = createTaskTypeLookup({ load: (projectId) => loadProjectTaskTypes({ projectId, listTools: () => mcpClient.listTools(), callTool: (name, args) => callMcpTool(name, args).then((result) => toolData(result, name)) }) })
|
|
999
1001
|
|
|
1000
1002
|
const callMcpReadWithRetry = async (name, args = {}) => {
|
|
1001
1003
|
try { return await callMcpTool(name, args) }
|
|
@@ -1006,21 +1008,21 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
1006
1008
|
}
|
|
1007
1009
|
|
|
1008
1010
|
const loadPendingTickets = async () => {
|
|
1009
|
-
const projectsData = toolData(await callMcpReadWithRetry('list_projects'))
|
|
1011
|
+
const projectsData = toolData(await callMcpReadWithRetry('list_projects'), 'list_projects')
|
|
1010
1012
|
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
1011
1013
|
const groups = await mapConcurrent(projects.filter((project) => project?.id != null), 3, async (project) => {
|
|
1012
1014
|
const [tasksResult, typesResult] = await Promise.all([
|
|
1013
1015
|
callMcpReadWithRetry('list_tasks', { project_id: project.id }),
|
|
1014
1016
|
taskTypeSets(project.id),
|
|
1015
1017
|
])
|
|
1016
|
-
const tasksData = toolData(tasksResult)
|
|
1018
|
+
const tasksData = toolData(tasksResult, 'list_tasks')
|
|
1017
1019
|
const { done: doneIds, review: reviewIds } = typesResult
|
|
1018
1020
|
const tasks = await mapConcurrent(Array.isArray(tasksData.tasks) ? tasksData.tasks : [], 3, async (task) => {
|
|
1019
1021
|
await resolveTaskOwner(task)
|
|
1020
1022
|
if (ticketDisplaySlug(task) || task?.id == null || !taskBelongsToAgent(task, { id: selfAgentId, identifier })) return task
|
|
1021
1023
|
// Some list responses omit the board slug. Resolve details by the real
|
|
1022
1024
|
// database identity; never interpret the number in OPEN-105 as an id.
|
|
1023
|
-
const data = toolData(await callMcpReadWithRetry('get_ticket', { project_id: project.id, ticket_id: task.id }))
|
|
1025
|
+
const data = toolData(await callMcpReadWithRetry('get_ticket', { project_id: project.id, ticket_id: task.id }), 'get_ticket')
|
|
1024
1026
|
const detail = data.ticket ?? data.task ?? data
|
|
1025
1027
|
if (String(detail.id) !== String(task.id) || (detail.project_id != null && String(detail.project_id) !== String(project.id))) throw new Error('Ticket detail identity did not match its assignment.')
|
|
1026
1028
|
return detail
|
|
@@ -1238,7 +1240,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
1238
1240
|
dedicatedChannelRead = (async () => {
|
|
1239
1241
|
try {
|
|
1240
1242
|
await ensureSelfAgent()
|
|
1241
|
-
const data = toolData(await callMcpTool('list_channels'))
|
|
1243
|
+
const data = toolData(await callMcpTool('list_channels'), 'list_channels')
|
|
1242
1244
|
const channels = data.channels ?? data.data?.channels ?? data.data ?? data.items ?? []
|
|
1243
1245
|
const channel = dedicatedChannel(Array.isArray(channels) ? channels : [], { id: selfAgentId, identifier, aliases: [...selfAliases], channelId: readConfig(slug)?.dedicatedChannelId })
|
|
1244
1246
|
const channelId = channel ? Number(channel.id) : null
|
|
@@ -1254,7 +1256,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
1254
1256
|
}
|
|
1255
1257
|
|
|
1256
1258
|
const announceIntroduction = async () => {
|
|
1257
|
-
const projectsData = toolData(await callMcpTool('list_projects'))
|
|
1259
|
+
const projectsData = toolData(await callMcpTool('list_projects'), 'list_projects')
|
|
1258
1260
|
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
1259
1261
|
const project = projects.find((item) => Number.isFinite(Number(item.id)))
|
|
1260
1262
|
if (!project) { log('no project available for first-connection introduction'); return false }
|
|
@@ -1423,7 +1425,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
1423
1425
|
if (!mcpUrl || backlogProbeBusy) return
|
|
1424
1426
|
backlogProbeBusy = true
|
|
1425
1427
|
try {
|
|
1426
|
-
const projectsData = toolData(await callMcpTool('list_projects'))
|
|
1428
|
+
const projectsData = toolData(await callMcpTool('list_projects'), 'list_projects')
|
|
1427
1429
|
const projects = Array.isArray(projectsData.projects) ? projectsData.projects : []
|
|
1428
1430
|
const mentionActivity = []
|
|
1429
1431
|
let activityReplayTouched = false
|
|
@@ -1431,7 +1433,7 @@ export function createBackendWatcher({ backend, wsUrl, apiKey, identifier, slug,
|
|
|
1431
1433
|
try {
|
|
1432
1434
|
const assigned = []
|
|
1433
1435
|
const [tasksData, typesData] = await Promise.all([
|
|
1434
|
-
callMcpTool('list_tasks', { project_id: project.id }).then(toolData),
|
|
1436
|
+
callMcpTool('list_tasks', { project_id: project.id }).then((result) => toolData(result, 'list_tasks')),
|
|
1435
1437
|
taskTypeSets(project.id),
|
|
1436
1438
|
])
|
|
1437
1439
|
const { done: doneIds, review: reviewIds } = typesData
|