diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index f4ac0d4f2c..45cc6b3946 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder +import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent +import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm @@ -454,6 +456,15 @@ class NotificationFeedFilter( return it.author?.pubkeyHex != loggedInUserHex } + // A finished or failed agent job I filed: the workspace bot addresses the outcome to me via a + // `p` tag = the requester. Notify me directly — I don't "follow" the bot and the job kinds aren't + // in the generic relevance path, so mirror the Buzz-DM early return above rather than the p-tag + // heuristic. This is activity (not a chat message), so it ignores the Messages toggle. + if (noteEvent is JobResultEvent || noteEvent is JobErrorEvent) { + val requester = (noteEvent as? JobResultEvent)?.requester() ?: (noteEvent as JobErrorEvent).requester() + return requester == loggedInUserHex && it.author?.pubkeyHex != loggedInUserHex + } + if (!showMessages && ( noteEvent is ChatMessageEvent || diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/BuzzAgentCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/BuzzAgentCommands.kt index ada709eeae..73fdab05f4 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/BuzzAgentCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/BuzzAgentCommands.kt @@ -414,7 +414,7 @@ object BuzzAgentCommands { git(opts.worktreeBase, "worktree", "add", "-B", branch, worktreePath, opts.baseRef) } if (add.exit != 0) { - publish(ctx, opts.relay, JobErrorEvent.build(job.jobId, "worktree setup failed: ${add.stderr.take(MAX_BODY)}", channel, "error")) + publish(ctx, opts.relay, JobErrorEvent.build(job.jobId, "worktree setup failed: ${add.stderr.take(MAX_BODY)}", channel, job.requester, "error")) return mapOf("job_id" to job.jobId, "state" to "failed", "error" to "worktree") } workdir = worktreePath @@ -446,7 +446,7 @@ object BuzzAgentCommands { mapOf("job_id" to job.jobId, "state" to "completed", "exit" to 0, "branch" to if (opts.worktreeBase != null) branch else null) } else { val body = (run.stderr.ifBlank { run.stdout }).ifBlank { "exited ${run.exit}" } - publish(ctx, opts.relay, JobErrorEvent.build(job.jobId, body.take(MAX_BODY), channel, "error")) + publish(ctx, opts.relay, JobErrorEvent.build(job.jobId, body.take(MAX_BODY), channel, job.requester, "error")) mapOf("job_id" to job.jobId, "state" to "failed", "exit" to run.exit) } } finally { diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/BuzzJobAggregatorTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/BuzzJobAggregatorTest.kt index a3e320ac92..8f56ad9d48 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/BuzzJobAggregatorTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/BuzzJobAggregatorTest.kt @@ -70,7 +70,7 @@ class BuzzJobAggregatorTest { } private fun error(createdAt: Long = 1040): JobErrorEvent { - val t = JobErrorEvent.build(jobId, "build failed", channel, "error", createdAt) + val t = JobErrorEvent.build(jobId, "build failed", channel, status = "error", createdAt = createdAt) return JobErrorEvent("6".repeat(64), agent, t.createdAt, t.tags, t.content, "sig") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEvent.kt index 82e2a69376..f011f5faa5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEvent.kt @@ -50,6 +50,9 @@ class JobErrorEvent( /** The channel this job is scoped to - the `h` tag. */ fun channel() = tags.jobChannel() + /** The requester the failure is addressed to - the `p` tag. */ + fun requester() = tags.jobParticipant() + /** The optional status token - the `status` tag. */ fun status() = tags.jobStatus() @@ -63,12 +66,14 @@ class JobErrorEvent( requestId: HexKey, error: String, channelId: String? = null, + requester: HexKey? = null, status: String? = null, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, error, createdAt) { jobRequest(requestId) channelId?.let { jobChannel(it) } + requester?.let { jobParticipant(it) } status?.let { jobStatus(it) } initializer() } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEventTest.kt index 95abf3c253..f5a7c5d23b 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/jobs/JobErrorEventTest.kt @@ -26,16 +26,18 @@ import kotlin.test.assertEquals class JobErrorEventTest { private val requestId = "e".repeat(64) private val channel = "3f2504e0-4f89-41d3-9a0c-0305e82c3301" + private val requester = "a".repeat(64) @Test fun buildTagsAndContent() { - val t = JobErrorEvent.build(requestId, "tool timeout", channel, "failed") + val t = JobErrorEvent.build(requestId, "tool timeout", channel, requester, "failed") val ev = JobErrorEvent("00", "f".repeat(64), t.createdAt, t.tags, t.content, "sig") assertEquals(JobErrorEvent.KIND, ev.kind) assertEquals("tool timeout", ev.error()) assertEquals(requestId, ev.jobRequest()) assertEquals(channel, ev.channel()) + assertEquals(requester, ev.requester()) assertEquals("failed", ev.status()) } }