Skip to content

Commit 5d630da

Browse files
committed
refactor(core): reuse dispatch idempotency for event tasks
1 parent 0f3632f commit 5d630da

11 files changed

Lines changed: 78 additions & 338 deletions

File tree

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
CREATE TABLE "junior_event_task_matches" (
2-
"id" text PRIMARY KEY NOT NULL,
3-
"task_id" text NOT NULL,
4-
"status" text NOT NULL,
5-
"created_at_ms" bigint NOT NULL,
6-
"match_json" jsonb NOT NULL
7-
);
8-
--> statement-breakpoint
91
CREATE TABLE "junior_event_tasks" (
102
"id" text PRIMARY KEY NOT NULL,
113
"team_id" text NOT NULL,
@@ -16,6 +8,5 @@ CREATE TABLE "junior_event_tasks" (
168
"task_json" jsonb NOT NULL
179
);
1810
--> statement-breakpoint
19-
CREATE INDEX "junior_event_task_matches_task_idx" ON "junior_event_task_matches" USING btree ("task_id","created_at_ms","id");--> statement-breakpoint
2011
CREATE INDEX "junior_event_tasks_team_idx" ON "junior_event_tasks" USING btree ("team_id","created_at_ms","id") WHERE "junior_event_tasks"."status" <> 'deleted';--> statement-breakpoint
2112
CREATE INDEX "junior_event_tasks_match_idx" ON "junior_event_tasks" USING btree ("provider","resource_ref","created_at_ms","id") WHERE "junior_event_tasks"."status" = 'active';

packages/junior/migrations/meta/0014_snapshot.json

Lines changed: 23 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "49fab1ec-994f-469b-9eb9-94c0d737bb9d",
2+
"id": "2d78fbea-150b-420d-bbc7-7fa4927211e0",
33
"prevId": "5d0244f5-7ca6-4a03-ad59-4699ba6bf0e1",
44
"version": "7",
55
"dialect": "postgresql",
@@ -155,25 +155,16 @@
155155
"name": "junior_conversation_annotations_conversation_id_fk",
156156
"tableFrom": "junior_conversation_annotations",
157157
"tableTo": "junior_conversations",
158-
"columnsFrom": [
159-
"conversation_id"
160-
],
161-
"columnsTo": [
162-
"conversation_id"
163-
],
158+
"columnsFrom": ["conversation_id"],
159+
"columnsTo": ["conversation_id"],
164160
"onDelete": "cascade",
165161
"onUpdate": "no action"
166162
}
167163
},
168164
"compositePrimaryKeys": {
169165
"junior_conversation_annotations_pk": {
170166
"name": "junior_conversation_annotations_pk",
171-
"columns": [
172-
"conversation_id",
173-
"plugin",
174-
"kind",
175-
"key"
176-
]
167+
"columns": ["conversation_id", "plugin", "kind", "key"]
177168
}
178169
},
179170
"uniqueConstraints": {},
@@ -333,23 +324,16 @@
333324
"name": "junior_conversation_events_conversation_id_junior_conversations_conversation_id_fk",
334325
"tableFrom": "junior_conversation_events",
335326
"tableTo": "junior_conversations",
336-
"columnsFrom": [
337-
"conversation_id"
338-
],
339-
"columnsTo": [
340-
"conversation_id"
341-
],
327+
"columnsFrom": ["conversation_id"],
328+
"columnsTo": ["conversation_id"],
342329
"onDelete": "no action",
343330
"onUpdate": "no action"
344331
}
345332
},
346333
"compositePrimaryKeys": {
347334
"junior_conversation_events_conversation_id_seq_pk": {
348335
"name": "junior_conversation_events_conversation_id_seq_pk",
349-
"columns": [
350-
"conversation_id",
351-
"seq"
352-
]
336+
"columns": ["conversation_id", "seq"]
353337
}
354338
},
355339
"uniqueConstraints": {},
@@ -706,77 +690,53 @@
706690
"name": "junior_conversations_destination_id_junior_destinations_id_fk",
707691
"tableFrom": "junior_conversations",
708692
"tableTo": "junior_destinations",
709-
"columnsFrom": [
710-
"destination_id"
711-
],
712-
"columnsTo": [
713-
"id"
714-
],
693+
"columnsFrom": ["destination_id"],
694+
"columnsTo": ["id"],
715695
"onDelete": "no action",
716696
"onUpdate": "no action"
717697
},
718698
"junior_conversations_actor_identity_id_junior_identities_id_fk": {
719699
"name": "junior_conversations_actor_identity_id_junior_identities_id_fk",
720700
"tableFrom": "junior_conversations",
721701
"tableTo": "junior_identities",
722-
"columnsFrom": [
723-
"actor_identity_id"
724-
],
725-
"columnsTo": [
726-
"id"
727-
],
702+
"columnsFrom": ["actor_identity_id"],
703+
"columnsTo": ["id"],
728704
"onDelete": "no action",
729705
"onUpdate": "no action"
730706
},
731707
"junior_conversations_creator_identity_id_junior_identities_id_fk": {
732708
"name": "junior_conversations_creator_identity_id_junior_identities_id_fk",
733709
"tableFrom": "junior_conversations",
734710
"tableTo": "junior_identities",
735-
"columnsFrom": [
736-
"creator_identity_id"
737-
],
738-
"columnsTo": [
739-
"id"
740-
],
711+
"columnsFrom": ["creator_identity_id"],
712+
"columnsTo": ["id"],
741713
"onDelete": "no action",
742714
"onUpdate": "no action"
743715
},
744716
"junior_conversations_credential_subject_identity_id_junior_identities_id_fk": {
745717
"name": "junior_conversations_credential_subject_identity_id_junior_identities_id_fk",
746718
"tableFrom": "junior_conversations",
747719
"tableTo": "junior_identities",
748-
"columnsFrom": [
749-
"credential_subject_identity_id"
750-
],
751-
"columnsTo": [
752-
"id"
753-
],
720+
"columnsFrom": ["credential_subject_identity_id"],
721+
"columnsTo": ["id"],
754722
"onDelete": "no action",
755723
"onUpdate": "no action"
756724
},
757725
"junior_conversations_parent_conversation_id_junior_conversations_conversation_id_fk": {
758726
"name": "junior_conversations_parent_conversation_id_junior_conversations_conversation_id_fk",
759727
"tableFrom": "junior_conversations",
760728
"tableTo": "junior_conversations",
761-
"columnsFrom": [
762-
"parent_conversation_id"
763-
],
764-
"columnsTo": [
765-
"conversation_id"
766-
],
729+
"columnsFrom": ["parent_conversation_id"],
730+
"columnsTo": ["conversation_id"],
767731
"onDelete": "no action",
768732
"onUpdate": "no action"
769733
},
770734
"junior_conversations_root_conversation_id_junior_conversations_conversation_id_fk": {
771735
"name": "junior_conversations_root_conversation_id_junior_conversations_conversation_id_fk",
772736
"tableFrom": "junior_conversations",
773737
"tableTo": "junior_conversations",
774-
"columnsFrom": [
775-
"root_conversation_id"
776-
],
777-
"columnsTo": [
778-
"conversation_id"
779-
],
738+
"columnsFrom": ["root_conversation_id"],
739+
"columnsTo": ["conversation_id"],
780740
"onDelete": "no action",
781741
"onUpdate": "no action"
782742
}
@@ -917,77 +877,6 @@
917877
"checkConstraints": {},
918878
"isRLSEnabled": false
919879
},
920-
"public.junior_event_task_matches": {
921-
"name": "junior_event_task_matches",
922-
"schema": "",
923-
"columns": {
924-
"id": {
925-
"name": "id",
926-
"type": "text",
927-
"primaryKey": true,
928-
"notNull": true
929-
},
930-
"task_id": {
931-
"name": "task_id",
932-
"type": "text",
933-
"primaryKey": false,
934-
"notNull": true
935-
},
936-
"status": {
937-
"name": "status",
938-
"type": "text",
939-
"primaryKey": false,
940-
"notNull": true
941-
},
942-
"created_at_ms": {
943-
"name": "created_at_ms",
944-
"type": "bigint",
945-
"primaryKey": false,
946-
"notNull": true
947-
},
948-
"match_json": {
949-
"name": "match_json",
950-
"type": "jsonb",
951-
"primaryKey": false,
952-
"notNull": true
953-
}
954-
},
955-
"indexes": {
956-
"junior_event_task_matches_task_idx": {
957-
"name": "junior_event_task_matches_task_idx",
958-
"columns": [
959-
{
960-
"expression": "task_id",
961-
"isExpression": false,
962-
"asc": true,
963-
"nulls": "last"
964-
},
965-
{
966-
"expression": "created_at_ms",
967-
"isExpression": false,
968-
"asc": true,
969-
"nulls": "last"
970-
},
971-
{
972-
"expression": "id",
973-
"isExpression": false,
974-
"asc": true,
975-
"nulls": "last"
976-
}
977-
],
978-
"isUnique": false,
979-
"concurrently": false,
980-
"method": "btree",
981-
"with": {}
982-
}
983-
},
984-
"foreignKeys": {},
985-
"compositePrimaryKeys": {},
986-
"uniqueConstraints": {},
987-
"policies": {},
988-
"checkConstraints": {},
989-
"isRLSEnabled": false
990-
},
991880
"public.junior_event_tasks": {
992881
"name": "junior_event_tasks",
993882
"schema": "",
@@ -1289,12 +1178,8 @@
12891178
"name": "junior_identities_user_id_junior_users_id_fk",
12901179
"tableFrom": "junior_identities",
12911180
"tableTo": "junior_users",
1292-
"columnsFrom": [
1293-
"user_id"
1294-
],
1295-
"columnsTo": [
1296-
"id"
1297-
],
1181+
"columnsFrom": ["user_id"],
1182+
"columnsTo": ["id"],
12981183
"onDelete": "no action",
12991184
"onUpdate": "no action"
13001185
}
@@ -1346,12 +1231,7 @@
13461231
"compositePrimaryKeys": {
13471232
"junior_stats_date_namespace_metric_name_pk": {
13481233
"name": "junior_stats_date_namespace_metric_name_pk",
1349-
"columns": [
1350-
"date",
1351-
"namespace",
1352-
"metric",
1353-
"name"
1354-
]
1234+
"columns": ["date", "namespace", "metric", "name"]
13551235
}
13561236
},
13571237
"uniqueConstraints": {},
@@ -1436,4 +1316,4 @@
14361316
"schemas": {},
14371317
"tables": {}
14381318
}
1439-
}
1319+
}

packages/junior/migrations/meta/_journal.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@
103103
{
104104
"idx": 14,
105105
"version": "7",
106-
"when": 1785525804752,
107-
"tag": "0014_fuzzy_toad_men",
106+
"when": 1785546394600,
107+
"tag": "0014_normal_kat_farrell",
108108
"breakpoints": true
109109
}
110110
]
111-
}
111+
}
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Event tasks
22

33
Core stores durable Slack-destination tasks that match normalized provider
4-
resource events by provider, resource ref, and event type. Provider plugins own webhook
5-
verification and event normalization; they publish through the core resource
6-
event boundary without knowing which conversations or tasks consume an event.
4+
resource events by provider, resource ref, and event type. Provider plugins own
5+
webhook verification and event normalization; they publish through the core
6+
resource event boundary without knowing which conversations or tasks consume an
7+
event.
78

89
Each matching task receives an independent idempotent agent dispatch. A failure
910
for one task does not prevent dispatch attempts for other matching tasks; the
1011
ingress boundary still receives the aggregate failure for provider retry. Task
1112
management is bound to the Slack destination where the task was created.
13+
One Junior deployment serves one Slack workspace, which is the event-task
14+
authorization boundary.

packages/junior/src/chat/event-tasks/ingest.ts

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import {
55
type ResourceEvent,
66
} from "@sentry/junior-plugin-api";
77
import { dispatchTask } from "@/chat/agent-dispatch/context";
8-
import { getDb, getSqlExecutor } from "@/chat/db";
9-
import {
10-
claimActiveEventTaskMatch,
11-
findMatchingEventTasks,
12-
markEventTaskMatchDispatched,
13-
} from "@/chat/event-tasks/store";
14-
import type { EventTask, EventTaskMatch } from "@/chat/event-tasks/types";
8+
import { getDb } from "@/chat/db";
9+
import { findMatchingEventTasks } from "@/chat/event-tasks/store";
10+
import type { EventTask } from "@/chat/event-tasks/types";
1511
import type { ConversationWorkQueue } from "@/chat/task-execution/queue";
1612

17-
function matchId(taskId: string, provider: string, eventKey: string): string {
18-
return `evmatch_${createHash("sha256")
13+
function eventTaskDispatchKey(
14+
taskId: string,
15+
provider: string,
16+
eventKey: string,
17+
): string {
18+
return `event-task:${createHash("sha256")
1919
.update(`${taskId}\0${provider}\0${eventKey}`)
2020
.digest("hex")
2121
.slice(0, 32)}`;
@@ -68,18 +68,11 @@ export async function ingestEventTasks(
6868
const errors: unknown[] = [];
6969
for (const task of tasks) {
7070
try {
71-
const id = matchId(task.id, event.provider, event.eventKey);
72-
const pending: EventTaskMatch = {
73-
id,
74-
createdAtMs: nowMs,
75-
eventKey: event.eventKey,
76-
eventType: event.eventType,
77-
status: "pending",
78-
taskId: task.id,
79-
};
80-
const match = await claimActiveEventTaskMatch(getSqlExecutor(), pending);
81-
if (!match) continue;
82-
if (match.status === "dispatched") continue;
71+
const idempotencyKey = eventTaskDispatchKey(
72+
task.id,
73+
event.provider,
74+
event.eventKey,
75+
);
8376
const credentialSubject =
8477
task.credentialMode === "creator"
8578
? {
@@ -93,7 +86,7 @@ export async function ingestEventTasks(
9386
conversationWorkQueue: options.queue,
9487
nowMs,
9588
options: {
96-
idempotencyKey: id,
89+
idempotencyKey,
9790
...(credentialSubject ? { credentialSubject } : {}),
9891
destination: task.destination,
9992
destinationVisibility:
@@ -116,8 +109,9 @@ export async function ingestEventTasks(
116109
}),
117110
},
118111
});
119-
await markEventTaskMatchDispatched(db, id, dispatch.id);
120-
dispatched += 1;
112+
if (dispatch.status === "created") {
113+
dispatched += 1;
114+
}
121115
} catch (error) {
122116
errors.push(error);
123117
}

0 commit comments

Comments
 (0)