mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat(buzz): notify the requester when their agent job finishes or fails
Finished/failed jobs now land in the Notifications tab, addressed to the requester. 1. NotificationFeedFilter: an early-return branch accepts a JobResultEvent (43004) or JobErrorEvent (43006) when it p-tags me (the requester) and isn't my own event — mirroring the existing Buzz-DM branch, since I don't "follow" the workspace bot and the job kinds aren't in the generic relevance path. It maps to the generic NoteCard, so it renders the result (PR URL) / error text. 2. JobErrorEvent now carries the requester as a `p` tag (new requester() accessor + a `requester` param on build, mirroring JobResultEvent); the scheduler passes job.requester on both error paths. Previously a failed job wasn't addressed to anyone, so a failure could never notify. Tests updated. Relay sourcing (verified): a job outcome reaches LocalCache via the always-on `#h` joined-group chat tail on the workspace relay (RELAY_GROUP_ALL_TIMELINE_KINDS includes 43001-43006), so for a shared channel the team has joined, results are pulled continuously and now notify. A channel you haven't joined (or a job event with no `#h`) would still need a dedicated `#p`=me subscription (mirroring BuzzDmDiscovery) — not added, since the support-channel model always has members joined. App compiles (fdroidDebug); quartz + commons tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mApqAbr8vkLC7gUDjavu6
This commit is contained in:
+11
@@ -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 ||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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<JobErrorEvent>.() -> Unit = {},
|
||||
) = eventTemplate<JobErrorEvent>(KIND, error, createdAt) {
|
||||
jobRequest(requestId)
|
||||
channelId?.let { jobChannel(it) }
|
||||
requester?.let { jobParticipant(it) }
|
||||
status?.let { jobStatus(it) }
|
||||
initializer()
|
||||
}
|
||||
|
||||
+3
-1
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user