openvisio-agent 0.19.11 → 0.19.12
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/scripts/certify.mjs +3 -2
- package/src/events.mjs +5 -4
- package/src/memory.mjs +1 -1
- package/src/thread-context.mjs +27 -0
- package/src/watch.mjs +47 -4
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -63,8 +63,9 @@ const assertions = [
|
|
|
63
63
|
['reconciled mentions reuse the guarded websocket delivery path', watcher.includes("onEvent('agent:mention'") && watcher.includes('_mentionAlreadyMarked: true')],
|
|
64
64
|
['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'")],
|
|
65
65
|
['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')],
|
|
66
|
-
['reconciliation recovers untagged human follow-ups in owned threads', watcher.includes("memory.has(activityControlKey, 'active')") && watcher.includes('const addressedActivity = mentionNeedles.some') && watcher.includes("
|
|
67
|
-
['coding mentions require an action and concrete repository target', watcher.includes('conversationNeedsCode(text
|
|
66
|
+
['reconciliation recovers untagged human follow-ups in owned threads', watcher.includes("memory.has(activityControlKey, 'active')") && watcher.includes('const addressedActivity = mentionNeedles.some') && watcher.includes("memory.has(activitySourceKey, 'received')")],
|
|
67
|
+
['coding mentions require an action and concrete repository target', watcher.includes('conversationNeedsCode(text, { rootContent:') && events.includes('const action =') && events.includes('const target =')],
|
|
68
|
+
['agent-authored roots establish ownership and legacy roots are verified before routing', watcher.includes('saveOwnThread(channelId, postedId, message)') && watcher.includes('await resolveOwnThread(cid, parent)') && watcher.includes('_threadRootContent: rootContent')],
|
|
68
69
|
['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'")],
|
|
69
70
|
['thread cancellation is rechecked immediately before watcher delivery', watcher.includes('const newlyCancelled = suppressCancelled()') && watcher.includes('if (newlyCancelled) return newlyCancelled')],
|
|
70
71
|
['generic coding pickup messages are absent', !watcher.includes("I've picked this up and will return here with the verified result")],
|
package/src/events.mjs
CHANGED
|
@@ -400,7 +400,7 @@ export function renderedAgentMessages(value, identity = {}) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
|
-
for (const key of ['messages', 'replies', 'items', 'data', 'result', 'thread', 'message']) {
|
|
403
|
+
for (const key of ['messages', 'replies', 'items', 'data', 'result', 'thread', 'message', 'root']) {
|
|
404
404
|
if (row[key] && typeof row[key] === 'object') visit(row[key], depth + 1)
|
|
405
405
|
}
|
|
406
406
|
}
|
|
@@ -487,12 +487,13 @@ export function classifyConversationTarget(message, selfAliases, { threadOwned =
|
|
|
487
487
|
// Require an actionable repository verb and a concrete code/repository object.
|
|
488
488
|
// Broad nouns such as "feature", "API", "file", or "documentation" on their
|
|
489
489
|
// own describe plenty of product conversations and must not start a coding lane.
|
|
490
|
-
export function conversationNeedsCode(value) {
|
|
490
|
+
export function conversationNeedsCode(value, { rootContent = '' } = {}) {
|
|
491
491
|
const text = String(value || '')
|
|
492
492
|
if (/\b(?:do\s+not|don't|dont|stop|cancel|stand\s+down)\b/i.test(text)) return false
|
|
493
|
-
const action = /\b(?:implement|fix|debug|refactor|change|update|add|remove|write|edit|test|build|deploy|release|migrate|wire|integrate)\b/i.test(text)
|
|
493
|
+
const action = /\b(?:implement|fix|debug|repair|rebuild|rerun|refactor|change|update|add|remove|write|edit|test|build|deploy|release|migrate|wire|integrate)\b/i.test(text)
|
|
494
494
|
const target = /\b(?:code|codebase|repository|repo|github|branch|commit|pull\s+request|pr|endpoint|route|component|page|screen|ui|function|class|database|migration|schema|package|typescript|javascript|python|swift|rust|golang|css|html|source\s+file|tests?)\b/i.test(text)
|
|
495
|
-
|
|
495
|
+
const contextualRepair = /\b(?:fix|debug|repair|rebuild|rerun|run\s+(?:the\s+)?(?:build|tests?))\b/i.test(text) && /\b(?:github\.com\/\S+\/pull\/\d+|pull\s+request|repository|repo|branch|commit)\b/i.test(rootContent)
|
|
496
|
+
return (action && target) || contextualRepair
|
|
496
497
|
}
|
|
497
498
|
|
|
498
499
|
export function conversationAsksPendingTickets(value) {
|
package/src/memory.mjs
CHANGED
|
@@ -155,5 +155,5 @@ export function createMastraMemory({ ledgerPath, databasePath, resourceId, maxNo
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const settled = async () => { await Promise.all(pending.values()); await memory.settled() }
|
|
158
|
-
return { remember, connect: ledger.connect, recall: ledger.recall, context, has: ledger.has, persist: ledger.persist, settled, provider: 'mastra-libsql' }
|
|
158
|
+
return { remember, connect: ledger.connect, recall: ledger.recall, context, has: ledger.has, get: ledger.get, persist: ledger.persist, settled, provider: 'mastra-libsql' }
|
|
159
159
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderedAgentMessages } from './events.mjs'
|
|
2
|
+
|
|
3
|
+
// An agent's own root post establishes a conversation, even if nobody tagged
|
|
4
|
+
// the agent before replying. Verify the exact root, never mere participation.
|
|
5
|
+
export function ownThreadRoot(thread, rootId, identity) {
|
|
6
|
+
return renderedAgentMessages(thread, identity).find((row) => String(row.id) === String(rootId)) || null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createThreadContextResolver({ read, save, cancelled, load, identity }) {
|
|
10
|
+
const pending = new Map()
|
|
11
|
+
return async (channelId, rootId) => {
|
|
12
|
+
if (channelId == null || rootId == null || cancelled(channelId, rootId)) return ''
|
|
13
|
+
const cached = read(channelId, rootId)
|
|
14
|
+
if (cached) return cached
|
|
15
|
+
const key = `${channelId}:${rootId}`
|
|
16
|
+
if (!pending.has(key)) {
|
|
17
|
+
const request = Promise.resolve().then(() => load(channelId, rootId)).then((thread) => {
|
|
18
|
+
const root = ownThreadRoot(thread, rootId, identity())
|
|
19
|
+
if (!root || cancelled(channelId, rootId)) return ''
|
|
20
|
+
save(channelId, rootId, root.content)
|
|
21
|
+
return root.content
|
|
22
|
+
}).finally(() => pending.delete(key))
|
|
23
|
+
pending.set(key, request)
|
|
24
|
+
}
|
|
25
|
+
return pending.get(key)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/watch.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { mapConcurrent } from './concurrency.mjs'
|
|
|
20
20
|
import { releaseAuthorizedPause } from './authorization-resume.mjs'
|
|
21
21
|
import { dedicatedChannel, repliesAfterSource } from './channel-routing.mjs'
|
|
22
22
|
import { createTaskTypeLookup } from './task-types.mjs'
|
|
23
|
+
import { createThreadContextResolver } from './thread-context.mjs'
|
|
23
24
|
import { assignmentRequest, assignmentStatusOnly, routeAssignments } from './assignment-routing.mjs'
|
|
24
25
|
import { modelProcessOptions, stopModelProcess } from './process-lifecycle.mjs'
|
|
25
26
|
import { createMastraAcpRunner } from './mastra-harness.mjs'
|
|
@@ -876,6 +877,25 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
876
877
|
const controlKey = threadControlKey(channelId, parentId)
|
|
877
878
|
if (controlKey) memory.remember({ key: controlKey, kind: 'thread', state: 'active', summary, refs: { channelId: Number(channelId), threadId: parentId } })
|
|
878
879
|
}
|
|
880
|
+
const saveOwnThread = (channelId, rootId, content) => {
|
|
881
|
+
memory.remember({ key: `own-root:${channelId}:${rootId}`, kind: 'thread', state: 'authored', summary: content, refs: { channelId: Number(channelId), threadId: rootId }, meta: { rootContent: content } })
|
|
882
|
+
if (!memory.has(threadControlKey(channelId, rootId), 'cancelled')) activateThread(channelId, rootId, content)
|
|
883
|
+
}
|
|
884
|
+
const resolveOwnThread = createThreadContextResolver({
|
|
885
|
+
read: (cid, root) => {
|
|
886
|
+
const cached = memory.get(`own-root:${cid}:${root}`)?.meta?.rootContent
|
|
887
|
+
if (cached) return cached
|
|
888
|
+
// Previous releases recorded the posted message id but not ownership.
|
|
889
|
+
// This also recovers roots when the thread API returns only replies.
|
|
890
|
+
const previous = memory.recall({ channelId: cid }, 1000).find((node) => node.kind === 'delivery' && node.state === 'rendered' && node.refs?.threadId == null && String(node.meta?.messageId) === String(root))
|
|
891
|
+
if (previous?.summary) saveOwnThread(cid, root, previous.summary)
|
|
892
|
+
return previous?.summary || ''
|
|
893
|
+
},
|
|
894
|
+
save: saveOwnThread,
|
|
895
|
+
cancelled: (cid, root) => memory.has(threadControlKey(cid, root), 'cancelled'),
|
|
896
|
+
identity: () => ({ id: selfAgentId, identifier, slug, name: slug }),
|
|
897
|
+
load: async (cid, root) => toolData(await callMcpTool('list_message_thread', { channel_id: Number(cid), message_id: Number(root) })),
|
|
898
|
+
})
|
|
879
899
|
const postMessageOnce = async ({ key, channelId, parentId, projectId, content, skipIfAnyAgentReply = false, sourceKey = '', sourceMessageId, allowCancelled = false }) => {
|
|
880
900
|
const deliveryKey = String(key || '')
|
|
881
901
|
if (parentId == null) {
|
|
@@ -923,6 +943,8 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
923
943
|
...(parentId != null ? { parent_id: Number(parentId) } : {}),
|
|
924
944
|
content: message,
|
|
925
945
|
}))
|
|
946
|
+
const postedId = result.id ?? result.message?.id ?? result.message_id
|
|
947
|
+
if (parentId == null && postedId != null) saveOwnThread(channelId, postedId, message)
|
|
926
948
|
deliveredReplies.add(deliveryKey); trimSeen(deliveredReplies); persistReplay()
|
|
927
949
|
memory.remember({ key: deliveryKey, kind: 'delivery', state: 'rendered', summary: message, refs: { channelId: Number(channelId), ...(parentId != null ? { threadId: Number(parentId) } : {}), ...(Number.isFinite(Number(projectId)) ? { projectId: Number(projectId) } : {}) }, meta: { messageId: result.id ?? result.message?.id ?? null } })
|
|
928
950
|
if (sourceKey) memory.connect(deliveryKey, sourceKey, 'responds_to')
|
|
@@ -1193,7 +1215,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1193
1215
|
// old tagged thread root can otherwise make another agent's new reply
|
|
1194
1216
|
// look addressed to this watcher. Explicit mentions and human follow-ups
|
|
1195
1217
|
// in a persistently owned thread both enter the guarded per-message path.
|
|
1196
|
-
const addressedActivity = mentionNeedles.some((needle) => activityText.includes(needle)) || ownedActivity
|
|
1218
|
+
const addressedActivity = mentionNeedles.some((needle) => activityText.includes(needle)) || ownedActivity || activityParentId != null
|
|
1197
1219
|
if (!seenActivities.has(activityKey) && addressedActivity && /message|mention|channel/i.test(text)) {
|
|
1198
1220
|
seenActivities.add(activityKey); trimSeen(seenActivities); activityReplayTouched = true
|
|
1199
1221
|
// The same logical mention may already have arrived over WebSocket.
|
|
@@ -1203,7 +1225,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1203
1225
|
if (markMentionHandled(activityMessage, activityChannelId)) {
|
|
1204
1226
|
const activityMentionKeys = mentionDedupeKeys(activityMessage, activityChannelId)
|
|
1205
1227
|
const activitySourceKey = `mention:${activityChannelId ?? '?'}:${activityMentionKeys.idKey || activityMentionKeys.signatureKey || activityThreadRoot}`
|
|
1206
|
-
if (
|
|
1228
|
+
if (memory.has(activitySourceKey, 'received')) continue
|
|
1207
1229
|
}
|
|
1208
1230
|
mentionActivity.push({
|
|
1209
1231
|
projectId: project.id,
|
|
@@ -1519,7 +1541,27 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1519
1541
|
}
|
|
1520
1542
|
}
|
|
1521
1543
|
|
|
1544
|
+
const mentionIntake = new Map()
|
|
1522
1545
|
function onEvent(k, d) {
|
|
1546
|
+
if (k !== 'agent:mention') return routeEvent(k, d)
|
|
1547
|
+
const cid = d?.channel_id ?? d?.channelId
|
|
1548
|
+
const message = d?.message || {}
|
|
1549
|
+
const parent = message.parent_id ?? message.parentId
|
|
1550
|
+
const key = `${cid}:${parent ?? message.id ?? message.message_id}`
|
|
1551
|
+
// Keep redirects ordered while a legacy root is being verified. Other
|
|
1552
|
+
// conversations can resolve and run independently.
|
|
1553
|
+
const next = (mentionIntake.get(key) || Promise.resolve()).then(async () => {
|
|
1554
|
+
const preliminary = classifyConversationTarget(message, [...selfAliases])
|
|
1555
|
+
const irrelevant = preliminary.action === 'ignore' && preliminary.reason !== 'unaddressed-thread-activity'
|
|
1556
|
+
const rootContent = parent == null || irrelevant || preliminary.action === 'stand_down' ? '' : await resolveOwnThread(cid, parent)
|
|
1557
|
+
routeEvent(k, { ...d, _threadRootContent: rootContent })
|
|
1558
|
+
}).catch((error) => log('mention thread verification failed: ' + (error?.message || error)))
|
|
1559
|
+
.finally(() => { if (mentionIntake.get(key) === next) mentionIntake.delete(key) })
|
|
1560
|
+
mentionIntake.set(key, next)
|
|
1561
|
+
return next
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function routeEvent(k, d) {
|
|
1523
1565
|
const raw = d && typeof d === 'object' ? d : {}
|
|
1524
1566
|
if (k === 'task:assigned' || k === 'task:updated') {
|
|
1525
1567
|
void handleTaskSignal(k, raw)
|
|
@@ -1629,13 +1671,14 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1629
1671
|
// Light up the live status the instant we pick this up; drain owns the
|
|
1630
1672
|
// subsequent working/typing heartbeat for its lane.
|
|
1631
1673
|
if (cid != null) sendStatus(cid, 'thinking')
|
|
1632
|
-
const codingMention = canCode && conversationNeedsCode(text)
|
|
1674
|
+
const codingMention = canCode && conversationNeedsCode(text, { rootContent: raw._threadRootContent })
|
|
1633
1675
|
const replyDelivery = conversationDelivery(codingMention ? 'result' : 'reply', !codingMention)
|
|
1634
|
-
|
|
1676
|
+
let ctx = cid != null
|
|
1635
1677
|
? replyDelivery?.watcherOwned
|
|
1636
1678
|
? `The watcher verified this source message is addressed to you in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". ${codingMention ? 'Complete the concrete repository work and verification first.' : 'Answer the request directly.'} Do NOT call post_message; it is intentionally unavailable. Return only the final reply as your final answer. The watcher will re-check the live thread and render it at most once.${who ? ` To mention the requester, use their exact full name "@${who}".` : ''} The complete message is already here; do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates.`
|
|
1637
1679
|
: `The watcher verified this source message is addressed to you in OpenVisio channel ${cid}${who ? ` by "${who}"` : ''}: "${text}". ${codingMention ? `This is concrete repository work. Start the coding flow now. You may send at most one concrete progress update after work begins, but do not stop there: continue the work and then send one distinct final result with evidence or a real blocker. Use post_message for the final result even if you already sent a progress message` : 'Send EXACTLY ONE reply with post_message'}: arguments: channel_id ${cid}${threadRoot != null ? `, parent_id ${threadRoot} (reply IN THAT THREAD, do not start a new top-level message)` : ''}, plus agent_identifier + agent_api_key from the AUTH line above. Never repeat the same message.${who ? ` To @mention them back, write their EXACT full name "@${who}". A mention only links when the name matches exactly.` : ''} The complete message is already here. Do not call get_resource, get_marching_orders, poll_inbox, list_mcp_resources, or list_mcp_resource_templates.${codingMention ? ' Stop only after the work and final result/blocker.' : ' After your single reply, STOP.'}`
|
|
1638
1680
|
: undefined
|
|
1681
|
+
if (ctx && raw._threadRootContent) ctx += ` Verified thread root authored by you (context, not a new request): ${JSON.stringify(raw._threadRootContent)}. For requested repairs, inspect the referenced PR and its branch, run the failing verification, and fix that existing work. A review/testing handoff does not prevent an explicit human repair request.`
|
|
1639
1682
|
if (codingMention) {
|
|
1640
1683
|
// Activity indicators make accepted work visible. Do not add a canned
|
|
1641
1684
|
// pickup message; the first channel message is the verified result or a
|