openvisio-agent 0.16.1 → 0.16.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/package.json +1 -1
- package/src/events.mjs +8 -0
- package/src/watch.mjs +23 -10
package/package.json
CHANGED
package/src/events.mjs
CHANGED
|
@@ -26,6 +26,14 @@ export function taskAgentId(task) {
|
|
|
26
26
|
return task.agent_id != null ? task.agent_id : (task.agentId != null ? task.agentId : null)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function taskIsCompleted(task, completedTypeIds = new Set()) {
|
|
30
|
+
if (!task || typeof task !== 'object') return false
|
|
31
|
+
if (task.deleted_at || task.deletedAt || task.completed_at || task.completedAt || task.closed_at || task.closedAt || task.archived_at || task.archivedAt) return true
|
|
32
|
+
if (completedTypeIds.has(Number(task.type_id ?? task.typeId ?? task.status_id ?? task.statusId))) return true
|
|
33
|
+
const state = [task.status, task.state, task.type?.name, task.task_type?.name].filter(Boolean).join(' ')
|
|
34
|
+
return /\b(?:done|complete|completed|closed|cancelled|canceled|archived|resolved)\b/i.test(state)
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// A mention event means this agent's name appeared somewhere, not necessarily
|
|
30
38
|
// that the request was addressed to it. Reject a later-agent hand-off before a
|
|
31
39
|
// model starts, while keeping explicitly shared requests addressed to both.
|
package/src/watch.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { homedir } from 'node:os'
|
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
11
|
import { OV_DIR, DEFAULT_WORKSPACE, readConfig, writeJson, configPath, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
|
|
12
12
|
import { connectAgentWs, assertWebSocket } from './ws.mjs'
|
|
13
|
-
import { requestTargetsLaterAgent, taskAgentId, taskFromEvent } from './events.mjs'
|
|
13
|
+
import { requestTargetsLaterAgent, taskAgentId, taskFromEvent, taskIsCompleted } from './events.mjs'
|
|
14
14
|
|
|
15
15
|
// Behaviour prompts. The openvisio-team MCP bridge requires the agent's
|
|
16
16
|
// credentials as ARGUMENTS on every tool call — those are injected at runtime by
|
|
@@ -73,7 +73,7 @@ const CODE_CHARTER = [
|
|
|
73
73
|
'',
|
|
74
74
|
'WORK ETHIC — how a reliable teammate behaves (this is the difference between useful and ignored):',
|
|
75
75
|
' 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.',
|
|
76
|
-
' 2. FINISH, then REPORT.
|
|
76
|
+
' 2. FINISH, then REPORT. Always update/move the ticket with update_ticket. Post a channel result ONLY when this cycle includes a specific source channel/thread from a human request, and reply in that thread. Backlog-only work has no implied audience: update the ticket with evidence and do not announce it in an unrelated channel.',
|
|
77
77
|
' 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.',
|
|
78
78
|
' 4. One reply per channel per cycle; answer several nudges together.',
|
|
79
79
|
'',
|
|
@@ -89,7 +89,7 @@ const CODE_FULL = [
|
|
|
89
89
|
' 3. CHANGE + VERIFY: Read/Edit/Write the files; run the tests or build if the repo has them.',
|
|
90
90
|
' 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.',
|
|
91
91
|
' 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.',
|
|
92
|
-
' 6. CLOSE THE LOOP: move/update the ticket with update_ticket
|
|
92
|
+
' 6. CLOSE THE LOOP: move/update the ticket with update_ticket. Reply with the summary + PR link only in a source thread explicitly supplied by the event. For backlog-only tickets, do not post_message or seek a channel to announce completion.',
|
|
93
93
|
'Bash is for git / gh / tests / clone ONLY — never to hunt for credentials (they are given to you above).',
|
|
94
94
|
].join('\n')
|
|
95
95
|
|
|
@@ -577,7 +577,15 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
577
577
|
// Mentions we've already reacted to (by message id) — the backend can re-deliver
|
|
578
578
|
// an agent:mention (reconnect replay, dup fan-out), which otherwise makes the
|
|
579
579
|
// agent reply to the SAME message twice.
|
|
580
|
-
const
|
|
580
|
+
const replayPath = join(OV_DIR, 'watch-' + slug + '-replay.json')
|
|
581
|
+
let replayState = {}
|
|
582
|
+
try { replayState = JSON.parse(readFileSync(replayPath, 'utf8')) } catch { /* first run */ }
|
|
583
|
+
const seenMentions = new Set(Array.isArray(replayState.seenMentions) ? replayState.seenMentions : [])
|
|
584
|
+
const seenActivities = new Set(Array.isArray(replayState.seenActivities) ? replayState.seenActivities : [])
|
|
585
|
+
const trimSeen = (set) => { while (set.size > 500) set.delete(set.values().next().value) }
|
|
586
|
+
const persistReplay = () => {
|
|
587
|
+
try { writeJson(replayPath, { seenMentions: [...seenMentions], seenActivities: [...seenActivities] }, true) } catch { /* best-effort */ }
|
|
588
|
+
}
|
|
581
589
|
// Context lines from the events themselves (the WS payload already carries the
|
|
582
590
|
// channel + message / task), so the agent acts on THEM directly instead of
|
|
583
591
|
// hoping poll_inbox re-surfaces the same item. Accumulated across coalesced
|
|
@@ -610,7 +618,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
610
618
|
}
|
|
611
619
|
const ensureMcpSession = async () => {
|
|
612
620
|
if (mcpSessionId) return
|
|
613
|
-
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.16.
|
|
621
|
+
const res = await mcpPost({ jsonrpc: '2.0', id: ++mcpRpcId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'openvisio-agent', version: '0.16.2' } } }, false)
|
|
614
622
|
if (!res.ok) throw new Error('MCP initialize HTTP ' + res.status)
|
|
615
623
|
await mcpPayload(res)
|
|
616
624
|
mcpSessionId = res.headers.get('mcp-session-id') || ''
|
|
@@ -660,11 +668,11 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
660
668
|
callMcpTool('list_task_types', { project_id: project.id }).then(toolData),
|
|
661
669
|
callMcpTool('list_activity', { project_id: project.id }).then(toolData),
|
|
662
670
|
])
|
|
663
|
-
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) =>
|
|
671
|
+
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) => /\b(?:done|complete|completed|closed|cancelled|canceled|archived|resolved)\b/i.test(String(t.name || ''))).map((t) => Number(t.id)))
|
|
664
672
|
for (const task of Array.isArray(tasksData.tasks) ? tasksData.tasks : []) {
|
|
665
673
|
const taskAgentId = Number(task.agent_id ?? task.agentId ?? task.agent?.id)
|
|
666
674
|
const taskIdent = String(task.agent?.identifier ?? task.agent?.slug ?? '')
|
|
667
|
-
if (!task
|
|
675
|
+
if (!taskIsCompleted(task, doneIds) && (taskAgentId === Number(self.id) || taskIdent === identifier)) {
|
|
668
676
|
assigned.push({ id: task.id, projectId: project.id, project: project.name, title: task.title, priority: task.priority, typeId: task.type_id ?? task.typeId })
|
|
669
677
|
}
|
|
670
678
|
}
|
|
@@ -673,9 +681,14 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
673
681
|
for (const item of activities) {
|
|
674
682
|
const text = JSON.stringify(item)
|
|
675
683
|
const lower = text.toLowerCase()
|
|
676
|
-
|
|
684
|
+
const activityKey = String(project.id) + ':' + String(item.id ?? item.message_id ?? item.messageId ?? text.slice(0, 500))
|
|
685
|
+
if (!seenActivities.has(activityKey) && mentionNeedles.some((needle) => lower.includes(needle)) && /message|mention|channel/i.test(text)) {
|
|
686
|
+
seenActivities.add(activityKey); trimSeen(seenActivities)
|
|
687
|
+
mentionActivity.push({ projectId: project.id, project: project.name, activity: item })
|
|
688
|
+
}
|
|
677
689
|
}
|
|
678
690
|
}
|
|
691
|
+
if (mentionActivity.length) persistReplay()
|
|
679
692
|
if (!assigned.length) lastTaskSignature = ''
|
|
680
693
|
else {
|
|
681
694
|
const signature = JSON.stringify(assigned)
|
|
@@ -684,7 +697,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
684
697
|
lastTaskSignature = signature
|
|
685
698
|
lastTaskTriggeredAt = Date.now()
|
|
686
699
|
log('backlog reconciliation found ' + assigned.length + ' assigned task(s) -> coding cycle')
|
|
687
|
-
void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately.
|
|
700
|
+
void drain('full', `Backlog reconciliation verified these open tasks are assigned to YOU: ${JSON.stringify(assigned)}. Start with the highest-priority/oldest one immediately. This is backlog-only work with NO source message or requester thread: do NOT post_message and do not announce progress or completion in any channel. 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, then update the ticket with the evidence and move it to done when appropriate.`)
|
|
688
701
|
}
|
|
689
702
|
}
|
|
690
703
|
const inboxSignature = JSON.stringify(mentionActivity.slice(-20)).slice(0, 4000)
|
|
@@ -827,7 +840,7 @@ function loopBackendWs({ wsUrl, apiKey, identifier, slug, claude, agent, mcpConf
|
|
|
827
840
|
// when the payload carries no id.
|
|
828
841
|
const dedupeKey = mid != null ? 'id:' + mid : 'sig:' + (cid != null ? cid : '?') + '|' + text.slice(0, 100)
|
|
829
842
|
if (seenMentions.has(dedupeKey)) { log('agent:mention (dup) — skipped'); return }
|
|
830
|
-
seenMentions.add(dedupeKey);
|
|
843
|
+
seenMentions.add(dedupeKey); trimSeen(seenMentions); persistReplay()
|
|
831
844
|
if (requestTargetsLaterAgent(text, [slug, identifier])) {
|
|
832
845
|
log('agent:mention addressed to a later-mentioned agent — skipped')
|
|
833
846
|
return
|