openvisio-agent 0.7.0 → 0.7.1

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 +38 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openvisio-agent",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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
@@ -5,7 +5,7 @@
5
5
  // than written to disk from a pasted heredoc.
6
6
 
7
7
  import { spawn, spawnSync } from 'node:child_process'
8
- import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
8
+ import { writeFileSync, mkdirSync, existsSync, readFileSync, unlinkSync } from 'node:fs'
9
9
  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'
@@ -29,6 +29,7 @@ const REPLY_DISCIPLINE = [
29
29
  ' • IS IT FOR YOU? Act ONLY on messages addressed to YOU — an @mention of your exact name, a direct question to you, or a reply to something YOU said or did. If a DIFFERENT agent or person was @mentioned or asked to do something, STAY OUT: do not answer for them and do not pick up their task. When it is not yours, posting nothing is the correct move.',
30
30
  ' • NO DUPLICATES. Before you post, scan the recent thread/channel for what YOU already said. If you already replied to or acknowledged this exact request, do NOT post again. One acknowledgement per task; one answer per question. While a task is in progress, post again ONLY when you have something genuinely NEW (a result, a link, a real blocker) — never re-post "on it".',
31
31
  ' • BE SURE BEFORE YOU SPEAK. Do not claim something is possible, done, or broken until you have actually verified it — call the tool, read the code, check the real state. Never assert then contradict yourself. If you are unsure, verify FIRST, then give ONE clear, final answer instead of thinking out loud across several messages.',
32
+ ' • NO INVENTED HISTORY. You have NO memory beyond the messages visible in THIS thread and what your tools return right now. Never fabricate past events, competitions, conversations, results, links, PR numbers, deploy URLs, or figures. If you are asked about something you have no actual record of, say plainly "I don\'t have a record of that" — do NOT make one up to play along or be helpful. Only state things you can see or verify.',
32
33
  ].join('\n')
33
34
 
34
35
  // ── CHAT-ONLY agents (no --workdir): chat/ticket tools, no code surface. ──────
@@ -131,6 +132,29 @@ const SESSION_IDLE_MS = 1200000
131
132
  const MAX_CYCLE_MS = 240000
132
133
  const MAX_CODE_CYCLE_MS = 900000
133
134
 
135
+ // Refuse to run a SECOND watcher for the same agent. Two watchers connect to the
136
+ // WS as the same agent and BOTH reply to every mention — the #1 cause of duplicate
137
+ // (and contradicting, if the two are different versions) messages. A pid lock file
138
+ // in ~/.openvisio makes the second start fail fast instead. Stale locks (dead pid)
139
+ // are taken over. Returns { release } or { conflict: <pid> }.
140
+ function acquireSingleInstance(key) {
141
+ const lockPath = join(OV_DIR, 'watch-' + key + '.lock')
142
+ try {
143
+ mkdirSync(OV_DIR, { recursive: true })
144
+ if (existsSync(lockPath)) {
145
+ const pid = parseInt(String(readFileSync(lockPath, 'utf8')).trim(), 10)
146
+ if (pid && pid !== process.pid) {
147
+ let alive = false
148
+ try { process.kill(pid, 0); alive = true } catch (e) { alive = !!(e && e.code === 'EPERM') }
149
+ if (alive) return { conflict: pid }
150
+ }
151
+ }
152
+ writeFileSync(lockPath, String(process.pid))
153
+ } catch { /* if the lock can't be written, don't block the agent from running */ }
154
+ const release = () => { try { if (parseInt(String(readFileSync(lockPath, 'utf8')).trim(), 10) === process.pid) unlinkSync(lockPath) } catch { /* already gone */ } }
155
+ return { release }
156
+ }
157
+
134
158
  export async function runWatch({ flags }) {
135
159
  const slug = flags.name ? slugify(String(flags.name)) : null
136
160
  const saved = slug ? readConfig(slug) : null
@@ -161,6 +185,19 @@ export async function runWatch({ flags }) {
161
185
  const model = String(flags.model || (saved && saved.model) || (agent === 'opencode' ? '' : 'sonnet'))
162
186
  const chatModel = String(flags['chat-model'] || (saved && saved.chatModel) || '')
163
187
 
188
+ // ONE watcher per agent. A second one (e.g. a manual `watch` alongside the
189
+ // background service, or a stale service) is the #1 cause of duplicate replies:
190
+ // both connect as the same agent and both answer every mention. Refuse to start.
191
+ if (!flags.install) {
192
+ const lock = acquireSingleInstance(slug || 'openvisio')
193
+ if (lock.conflict) {
194
+ fail(`Another openvisio-agent watcher for "${slug || 'openvisio'}" is already running (pid ${lock.conflict}).\n` +
195
+ ` Two watchers for the same agent BOTH reply to every mention — that is what causes duplicate/contradicting messages.\n` +
196
+ ` Stop the other one (kill ${lock.conflict}), or rely on ONLY the background service. Refusing to start a second.`)
197
+ }
198
+ process.on('exit', () => { try { lock.release && lock.release() } catch { /* noop */ } })
199
+ }
200
+
164
201
  // Backend agents (connect --backend) drive autonomy over a real-time WS instead
165
202
  // of REST-polling the frontend relay. Detected by the saved mode / a --ws flag.
166
203
  const backendMode = (saved && saved.mode === 'backend') || !!flags.ws