openvisio-agent 0.19.1 → 0.19.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/README.md +1 -1
- package/package.json +1 -1
- package/scripts/certify.mjs +2 -1
- package/src/events.mjs +8 -5
- package/src/watch.mjs +13 -5
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ The backend MCP may be stateful or stateless. A successful initialize response w
|
|
|
66
66
|
|
|
67
67
|
Codex BYO agents follow the repository's normative runtime specification in `docs/CODEX_BYO_AGENT_SPEC.md`: one WebSocket identity, independent Sol reply/work lanes, authoritative `get_ticket` verification for assignments, REST-backed in-app activity, persistent replay suppression, and runtime evidence gates before completion. Maintainers must run `npm run certify` before publishing.
|
|
68
68
|
|
|
69
|
-
An `agent:mention` event only wakes the watcher;
|
|
69
|
+
An `agent:mention` event only wakes the watcher; the actual source message is checked before any model starts. An explicit tag establishes durable ownership of that thread, so later human follow-ups remain addressed to the agent without another @mention. Messages redirected to another agent and unaddressed agent chatter stay silent, while a direct stand-down cancels queued/running work and releases ownership for that thread. The watcher does not broadcast working, thinking, or typing presence updates. Claude and OpenCode may add one concrete progress update after work begins, but must continue and post a distinct verified result or blocker afterward; Codex keeps cancellation-safe delivery watcher-owned and renders the verified final answer once.
|
|
70
70
|
|
|
71
71
|
Ticket references follow the board UI: BYO agents use the project-scoped slug, such as `OVS-57`, in messages, comments, PR descriptions, blockers, and results. Numeric `project_id` and `ticket_id` values remain internal MCP arguments and are never used as human-facing ticket names. If an older backend omits the slug, the agent uses the ticket title rather than inventing one.
|
|
72
72
|
|
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -56,7 +56,8 @@ const assertions = [
|
|
|
56
56
|
['single-watcher acquisition is atomic and fails closed', watcher.includes("openSync(lockPath, 'wx')") && watcher.includes('Could not acquire the single-watcher lock')],
|
|
57
57
|
['websocket and activity mention delivery share a replay guard', watcher.includes('markMentionHandled(activityMessage, activityChannelId)') && watcher.includes('markMentionHandled(msg, cid)') && watcher.includes('recentMentionSignatures')],
|
|
58
58
|
['reconciled mentions reuse the guarded websocket delivery path', watcher.includes("onEvent('agent:mention'") && watcher.includes('_mentionAlreadyMarked: true')],
|
|
59
|
-
['conversation wake events
|
|
59
|
+
['conversation wake events preserve owned-thread follow-ups and filter other recipients before model start', watcher.includes("memory.has(controlKey, 'active')") && watcher.includes('classifyConversationTarget(msg, [...selfAliases], { threadOwned })') && events.includes("reason: 'owned-thread-follow-up'") && events.includes("reason: 'explicit-other-recipient'") && events.includes("reason: 'other-agent-chatter'")],
|
|
60
|
+
['prematurely acknowledged owned-thread replies recover exactly once', watcher.includes("!threadOwned || memory.has(sourceKey, 'received')") && watcher.includes('agent:mention replay recovered for active owned thread')],
|
|
60
61
|
['coding mentions require an action and concrete repository target', watcher.includes('conversationNeedsCode(text)') && events.includes('const action =') && events.includes('const target =')],
|
|
61
62
|
['stand-down and redirects cancel only source-thread workers', watcher.includes('cancelThread(cid, threadRoot') && watcher.includes('item.control.cancelled = true') && watcher.includes('item.control.runner?.cancelCurrent') && watcher.includes("subtype === 'canceled'")],
|
|
62
63
|
['thread cancellation is rechecked immediately before watcher delivery', watcher.includes('const newlyCancelled = suppressCancelled()') && watcher.includes('if (newlyCancelled) return newlyCancelled')],
|
package/src/events.mjs
CHANGED
|
@@ -450,7 +450,7 @@ export function messageSenderIsAgent(message) {
|
|
|
450
450
|
// `agent:mention`, even when the new message names somebody else. Treat the event
|
|
451
451
|
// name as a wake-up hint only: recipient ownership is decided from the actual
|
|
452
452
|
// message before a model or work lane is allowed to run.
|
|
453
|
-
export function classifyConversationTarget(message, selfAliases) {
|
|
453
|
+
export function classifyConversationTarget(message, selfAliases, { threadOwned = false } = {}) {
|
|
454
454
|
const m = message && typeof message === 'object' ? message : {}
|
|
455
455
|
const text = String(m.content ?? m.body ?? m.text ?? m.message ?? '').replace(/\s+/g, ' ').trim()
|
|
456
456
|
const { self: selfMentions, other: otherMentions, all: mentions } = conversationMentions(text, selfAliases)
|
|
@@ -471,11 +471,14 @@ export function classifyConversationTarget(message, selfAliases) {
|
|
|
471
471
|
if (explicitSelf && requestTargetsLaterAgent(text, selfAliases)) {
|
|
472
472
|
return { action: 'ignore', reason: 'redirected-to-later-agent', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
|
|
473
473
|
}
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
474
|
+
// An explicit mention establishes durable thread ownership in the watcher.
|
|
475
|
+
// Later human follow-ups in that same active thread remain addressed to this
|
|
476
|
+
// agent without requiring another @mention. Redirects, stand-downs and agent
|
|
477
|
+
// chatter were handled above and still fail closed.
|
|
478
478
|
if (!explicitSelf) {
|
|
479
|
+
if (threadOwned) {
|
|
480
|
+
return { action: 'handle', reason: 'owned-thread-follow-up', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
|
|
481
|
+
}
|
|
479
482
|
return { action: 'ignore', reason: 'unaddressed-thread-activity', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
|
|
480
483
|
}
|
|
481
484
|
return { action: 'handle', reason: 'explicit-self-recipient', text, explicitSelf, otherMentions: otherMentions.map((item) => item.name) }
|
package/src/watch.mjs
CHANGED
|
@@ -1709,11 +1709,21 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1709
1709
|
const mid = msg.id != null ? msg.id : (msg.message_id != null ? msg.message_id : null)
|
|
1710
1710
|
const parent = msg.parent_id != null ? msg.parent_id : (msg.parentId != null ? msg.parentId : null)
|
|
1711
1711
|
const threadRoot = parent != null ? parent : mid
|
|
1712
|
+
const controlKey = threadControlKey(cid, threadRoot)
|
|
1713
|
+
const threadOwned = !!controlKey && memory.has(controlKey, 'active')
|
|
1714
|
+
const mentionKeys = mentionDedupeKeys(msg, cid)
|
|
1715
|
+
const sourceKey = `mention:${cid ?? '?'}:${mentionKeys.idKey || mentionKeys.signatureKey || threadRoot || Date.now()}`
|
|
1712
1716
|
// De-dupe: the same mention re-delivered (reconnect replay / dup fan-out) must
|
|
1713
1717
|
// NOT trigger a second reply. Key by message id, or a channel+text signature
|
|
1714
|
-
// when the payload carries no id.
|
|
1715
|
-
|
|
1716
|
-
|
|
1718
|
+
// when the payload carries no id. Older recipient filtering acknowledged
|
|
1719
|
+
// some owned-thread follow-ups before routing them. If such a message is
|
|
1720
|
+
// replayed, its replay key exists but its received ledger entry does not;
|
|
1721
|
+
// admit it once so the corrected ownership rule can recover the message.
|
|
1722
|
+
if (!raw._mentionAlreadyMarked && markMentionHandled(msg, cid)) {
|
|
1723
|
+
if (!threadOwned || memory.has(sourceKey, 'received')) { log('agent:mention (dup) — skipped'); return }
|
|
1724
|
+
log('agent:mention replay recovered for active owned thread')
|
|
1725
|
+
}
|
|
1726
|
+
const target = classifyConversationTarget(msg, [...selfAliases], { threadOwned })
|
|
1717
1727
|
if (target.action === 'ignore') {
|
|
1718
1728
|
log('agent:mention ignored before model start (' + target.reason + ')')
|
|
1719
1729
|
// A redirect or another agent taking over invalidates any older work or
|
|
@@ -1724,8 +1734,6 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1724
1734
|
}
|
|
1725
1735
|
return
|
|
1726
1736
|
}
|
|
1727
|
-
const mentionKeys = mentionDedupeKeys(msg, cid)
|
|
1728
|
-
const sourceKey = `mention:${cid ?? '?'}:${mentionKeys.idKey || mentionKeys.signatureKey || threadRoot || Date.now()}`
|
|
1729
1737
|
if (target.action === 'stand_down') {
|
|
1730
1738
|
if (cid != null && threadRoot != null) {
|
|
1731
1739
|
cancelThread(cid, threadRoot, text || 'Explicit stand-down')
|