openvisio-agent 0.19.2 → 0.19.4
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/bin/cli.mjs +0 -0
- package/package.json +1 -1
- package/scripts/certify.mjs +2 -0
- package/src/events.mjs +9 -0
- package/src/watch.mjs +27 -10
package/bin/cli.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/scripts/certify.mjs
CHANGED
|
@@ -58,6 +58,7 @@ const assertions = [
|
|
|
58
58
|
['reconciled mentions reuse the guarded websocket delivery path', watcher.includes("onEvent('agent:mention'") && watcher.includes('_mentionAlreadyMarked: true')],
|
|
59
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
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')],
|
|
61
|
+
['reconciliation recovers untagged human follow-ups in owned threads', watcher.includes("memory.has(activityControlKey, 'active')") && watcher.includes('const addressedActivity = mentionNeedles.some') && watcher.includes("!ownedActivity || memory.has(activitySourceKey, 'received')")],
|
|
61
62
|
['coding mentions require an action and concrete repository target', watcher.includes('conversationNeedsCode(text)') && events.includes('const action =') && events.includes('const target =')],
|
|
62
63
|
['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'")],
|
|
63
64
|
['thread cancellation is rechecked immediately before watcher delivery', watcher.includes('const newlyCancelled = suppressCancelled()') && watcher.includes('if (newlyCancelled) return newlyCancelled')],
|
|
@@ -126,6 +127,7 @@ const assertions = [
|
|
|
126
127
|
['Codex recognizes helper authorization as a blocker', events.includes('OPENVISIO_PR_PUSH_AUTH_REQUIRED') && watcher.includes("block?.kind === 'pr-push-authorization-required'")],
|
|
127
128
|
['policy blocker is surfaced to the user', watcher.includes('reportPolicyBlock(prompt, activeTaskRef, result.policyBlock, delivery)') && watcher.includes("Action required: I'm blocked")],
|
|
128
129
|
['blocker routing carries explicit task identity', watcher.includes('const activeTaskRef = taskRef') && watcher.includes('taskRef: activeTaskRef')],
|
|
130
|
+
['reply discovery failures stay scoped while failed mutations fail closed', watcher.includes('blockingReplyMcpErrors(result?.mcpErrors)') && watcher.includes('preserving the scoped model reply') && events.includes('export function blockingReplyMcpErrors')],
|
|
129
131
|
['all runtime blockers have a delivery path', watcher.includes('publishBlocker') && watcher.includes('WORK_CYCLE_BLOCKED') && watcher.includes('COORDINATION_CYCLE_BLOCKED')],
|
|
130
132
|
['ticket blocker cannot self-authorize', watcher.includes("ticketNotice = `I'm paused") && watcher.includes('publishBlocker({ prompt, taskRef, delivery, notice, ticketNotice, pause: true })') && !watcher.includes('test(approvalText)')],
|
|
131
133
|
['agent messages use first-person voice', watcher.includes('FIRST-PERSON VOICE') && watcher.includes("I'm blocked") && !watcher.includes('Alex is blocked') && !watcher.includes('Alex is paused')],
|
package/src/events.mjs
CHANGED
|
@@ -495,6 +495,15 @@ export function conversationNeedsCode(value) {
|
|
|
495
495
|
return action && target
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
// Read-only discovery failures in a reply cycle are not failed mutations and
|
|
499
|
+
// must not be inflated into a generic user-facing blocker. The model can give a
|
|
500
|
+
// precise, scoped answer (or say which live fact it could not read). Failed team
|
|
501
|
+
// mutations remain fail-closed so prose can never masquerade as a completed act.
|
|
502
|
+
export function blockingReplyMcpErrors(errors) {
|
|
503
|
+
const mutation = /^(?:update_ticket|post_message|comment_ticket|react_message|create_codebase_branch|create_codebase_commit|write_codebase_file|create_pull_request)$/
|
|
504
|
+
return [...new Set((Array.isArray(errors) ? errors : []).map((name) => String(name || '').replace(/^.*(?:__|[.:/])/, '').replace(/[-.]/g, '_').toLowerCase()).filter((name) => mutation.test(name)))]
|
|
505
|
+
}
|
|
506
|
+
|
|
498
507
|
// Coordination tickets are imperative board/chat actions, not implementation
|
|
499
508
|
// work that merely mentions words such as "message", "status", or "label" in
|
|
500
509
|
// its feature title. Requiring the coordination verb at the start prevents
|
package/src/watch.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { join, dirname } from 'node:path'
|
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { OV_DIR, DEFAULT_WORKSPACE, readConfig, writeJson, configPath, onPath, fail, ok, info, slugify, stripSlash, chmodSafe } from './lib.mjs'
|
|
13
13
|
import { connectAgentWs, assertWebSocket } from './ws.mjs'
|
|
14
|
-
import { agentAddedByName, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, failedTaskRevisionIsCurrent, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, taskIsCoordinationOnly, taskRevision, ticketDisplaySlug } from './events.mjs'
|
|
14
|
+
import { agentAddedByName, blockingReplyMcpErrors, buildTaskCompletionReport, claudeEventEvidence, classifyConversationTarget, codexEventEvidence, codexPolicyBlock, combineRuntimeWorkEvidence, conversationNeedsCode, failedTaskRevisionIsCurrent, mentionDedupeKeys, missingRuntimeWorkEvidence, normalizeRenderedMessageText, opencodeEventEvidence, renderedAgentMessages, shouldSuppressCodexDiagnostic, taskAgentId, taskFromEvent, taskIsAwaitingReview, taskIsCompleted, taskIsCoordinationOnly, taskRevision, ticketDisplaySlug } from './events.mjs'
|
|
15
15
|
import { createMastraMemory } from './memory.mjs'
|
|
16
16
|
import { repositoryHasPrPushAuthorization } from './pr-push.mjs'
|
|
17
17
|
import { createMcpHttpClient } from './mcp-http.mjs'
|
|
@@ -834,7 +834,7 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
834
834
|
const fullPrompt = (canCode ? CODE_FULL + codexPushGuide : 'Handle the supplied verified backend ticket with the available OpenVisio tools. Update or comment on the ticket as requested, do not claim repository work in chat-only mode, and stop after the verified action.') + '\n\n' + backendToolRule
|
|
835
835
|
const fastPrompt = (canCode ? CODE_FAST : CYCLE_FAST) + '\n\n' + backendToolRule
|
|
836
836
|
const coordinatePrompt = COORDINATE + '\n\n' + backendToolRule
|
|
837
|
-
const guardedReplyPrompt =
|
|
837
|
+
const guardedReplyPrompt = `WATCHER-DELIVERED REPLY: the watcher has already verified that the current source message is addressed to you. Your current OpenVisio agent identifier is "${identifier}"; do not call list_agents merely to rediscover yourself. Do not call post_message, relay inbox tools, or MCP resource APIs. OpenVisio team-state tools are allowed: when the answer depends on live projects, assignments, tickets, or status, use list_projects/list_tasks/get_ticket as needed and never ask the teammate for a slug that those tools can resolve. A failed read-only discovery call is not a completed action and must not be described as an intervention-level blocker; continue with available live data or state the narrow fact you could not verify. Return only one natural, context-specific reply of 1-3 sentences. Do not echo the request, announce a plan, or add a generic acknowledgement. If the supplied context says the work was completed, lead with the verified result; if it is blocked, name only the real blocker and next action.` + '\n\n' + backendToolRule
|
|
838
838
|
// Live model state — changeable at runtime by the in-chat `/model` command.
|
|
839
839
|
// codeModel drives full/sweep cycles; chatModel (if set) the lighter fast/intro
|
|
840
840
|
// ones, so routine chatter can run cheaper than real code work.
|
|
@@ -1361,15 +1361,28 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1361
1361
|
? item.message
|
|
1362
1362
|
: data?.message && typeof data.message === 'object' ? data.message : item
|
|
1363
1363
|
const activityText = String(activityMessage?.content ?? activityMessage?.body ?? activityMessage?.text ?? '').toLowerCase()
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1364
|
+
const activityChannelId = item?.channel_id ?? item?.channelId ?? data?.channel_id ?? data?.channelId
|
|
1365
|
+
const activityMessageId = activityMessage?.id ?? activityMessage?.message_id ?? activityMessage?.messageId
|
|
1366
|
+
const activityParentId = activityMessage?.parent_id ?? activityMessage?.parentId
|
|
1367
|
+
const activityThreadRoot = activityParentId != null ? activityParentId : activityMessageId
|
|
1368
|
+
const activityControlKey = threadControlKey(activityChannelId, activityThreadRoot)
|
|
1369
|
+
const ownedActivity = !!activityControlKey && memory.has(activityControlKey, 'active')
|
|
1370
|
+
// Search only the source message, never the whole activity envelope: an
|
|
1371
|
+
// old tagged thread root can otherwise make another agent's new reply
|
|
1372
|
+
// look addressed to this watcher. Explicit mentions and human follow-ups
|
|
1373
|
+
// in a persistently owned thread both enter the guarded per-message path.
|
|
1374
|
+
const addressedActivity = mentionNeedles.some((needle) => activityText.includes(needle)) || ownedActivity
|
|
1375
|
+
if (!seenActivities.has(activityKey) && addressedActivity && /message|mention|channel/i.test(text)) {
|
|
1369
1376
|
seenActivities.add(activityKey); trimSeen(seenActivities); activityReplayTouched = true
|
|
1370
1377
|
// The same logical mention may already have arrived over WebSocket.
|
|
1371
1378
|
// Share the id/signature guard instead of starting a second model turn.
|
|
1372
|
-
|
|
1379
|
+
// Recover the former pre-routing acknowledgement bug exactly once
|
|
1380
|
+
// when an owned-thread source has no received ledger entry.
|
|
1381
|
+
if (markMentionHandled(activityMessage, activityChannelId)) {
|
|
1382
|
+
const activityMentionKeys = mentionDedupeKeys(activityMessage, activityChannelId)
|
|
1383
|
+
const activitySourceKey = `mention:${activityChannelId ?? '?'}:${activityMentionKeys.idKey || activityMentionKeys.signatureKey || activityThreadRoot}`
|
|
1384
|
+
if (!ownedActivity || memory.has(activitySourceKey, 'received')) continue
|
|
1385
|
+
}
|
|
1373
1386
|
mentionActivity.push({
|
|
1374
1387
|
projectId: project.id,
|
|
1375
1388
|
project: project.name,
|
|
@@ -1517,13 +1530,17 @@ function loopBackendWs({ backend, wsUrl, apiKey, identifier, slug, claude, agent
|
|
|
1517
1530
|
finally { if (activeTaskRef) await finalizeFailedTaskPause(activeTaskRef) }
|
|
1518
1531
|
return
|
|
1519
1532
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1533
|
+
const replyMutationErrors = kind !== 'full' ? blockingReplyMcpErrors(result?.mcpErrors) : []
|
|
1534
|
+
if (replyMutationErrors.length) {
|
|
1535
|
+
const notice = `I couldn't complete the requested OpenVisio action: ${replyMutationErrors.join(', ')}. I haven't claimed that change succeeded.`
|
|
1522
1536
|
log('COORDINATION_CYCLE_BLOCKED failed MCP calls; publishing blocker')
|
|
1523
1537
|
try { await publishBlocker({ prompt, taskRef: activeTaskRef, delivery, notice }) }
|
|
1524
1538
|
catch (e) { log('failed to publish coordination blocker: ' + (e?.message || e)) }
|
|
1525
1539
|
return
|
|
1526
1540
|
}
|
|
1541
|
+
if (kind !== 'full' && result?.mcpErrors?.length) {
|
|
1542
|
+
log('reply cycle had read-only MCP failures (' + result.mcpErrors.join(', ') + '); preserving the scoped model reply')
|
|
1543
|
+
}
|
|
1527
1544
|
// Model prose never proves success or a blocker. Full cycles must produce
|
|
1528
1545
|
// runtime-observed ticket reads, repository evidence, and ticket updates.
|
|
1529
1546
|
const ticketCycle = !!activeTaskRef || /ticket\s+#?\d+.*?project\s+\d+/i.test(prompt)
|