@opentag/store 0.3.0 → 0.3.2

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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/repository.ts
2
+ import { createHash } from "crypto";
2
3
  import {
3
4
  ApprovalDecisionSchema,
4
5
  ApplyIntentOutcomeSchema,
@@ -13,6 +14,7 @@ import {
13
14
  PolicyRuleSchema,
14
15
  ProposalLineageSchema,
15
16
  preflightMutationIntent,
17
+ formatProjectTargetRef,
16
18
  projectTargetRefFromEvent,
17
19
  protocolRunFieldsFromEvent,
18
20
  RunAdmissionDecisionSchema,
@@ -20,7 +22,7 @@ import {
20
22
  RunEventVisibilitySchema,
21
23
  SuggestedChangesSnapshotSchema
22
24
  } from "@opentag/core";
23
- import { and, asc, eq, inArray } from "drizzle-orm";
25
+ import { and, asc, desc, eq, inArray, lt } from "drizzle-orm";
24
26
 
25
27
  // src/schema.ts
26
28
  import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
@@ -93,6 +95,35 @@ var runEvents = sqliteTable(
93
95
  runIdx: index("run_events_run_idx").on(table.runId)
94
96
  })
95
97
  );
98
+ var sourceDeliveries = sqliteTable(
99
+ "source_deliveries",
100
+ {
101
+ source: text("source").notNull(),
102
+ deliveryId: text("delivery_id").notNull(),
103
+ runId: text("run_id").notNull(),
104
+ eventId: text("event_id").notNull(),
105
+ createdAt: text("created_at").notNull()
106
+ },
107
+ (table) => ({
108
+ pk: primaryKey({ columns: [table.source, table.deliveryId] }),
109
+ runIdx: index("source_deliveries_run_idx").on(table.runId)
110
+ })
111
+ );
112
+ var controlPlaneEvents = sqliteTable(
113
+ "control_plane_events",
114
+ {
115
+ id: integer("id").primaryKey({ autoIncrement: true }),
116
+ type: text("type").notNull(),
117
+ severity: text("severity").notNull(),
118
+ subject: text("subject"),
119
+ payloadJson: text("payload_json").notNull(),
120
+ createdAt: text("created_at").notNull()
121
+ },
122
+ (table) => ({
123
+ typeIdx: index("control_plane_events_type_idx").on(table.type),
124
+ severityIdx: index("control_plane_events_severity_idx").on(table.severity)
125
+ })
126
+ );
96
127
  var suggestedChanges = sqliteTable("suggested_changes", {
97
128
  proposalId: text("proposal_id").primaryKey(),
98
129
  runId: text("run_id").notNull(),
@@ -194,6 +225,7 @@ var callbackDeliveries = sqliteTable(
194
225
  uri: text("uri").notNull(),
195
226
  body: text("body").notNull(),
196
227
  threadKey: text("thread_key"),
228
+ idempotencyKey: text("idempotency_key"),
197
229
  metadataJson: text("metadata_json"),
198
230
  status: text("status").notNull(),
199
231
  attempts: integer("attempts").notNull().default(0),
@@ -204,7 +236,8 @@ var callbackDeliveries = sqliteTable(
204
236
  },
205
237
  (table) => ({
206
238
  callbackRunIdx: index("callback_deliveries_run_idx").on(table.runId),
207
- callbackStatusIdx: index("callback_deliveries_status_idx").on(table.status)
239
+ callbackStatusIdx: index("callback_deliveries_status_idx").on(table.status),
240
+ callbackIdempotencyIdx: uniqueIndex("callback_deliveries_idempotency_key_idx").on(table.idempotencyKey)
208
241
  })
209
242
  );
210
243
  function migrateSchema(sqlite) {
@@ -247,6 +280,28 @@ function migrateSchema(sqlite) {
247
280
  created_at TEXT NOT NULL
248
281
  );
249
282
  CREATE INDEX IF NOT EXISTS run_events_run_idx ON run_events(run_id);
283
+ CREATE TABLE IF NOT EXISTS source_deliveries (
284
+ source TEXT NOT NULL,
285
+ delivery_id TEXT NOT NULL,
286
+ run_id TEXT NOT NULL,
287
+ event_id TEXT NOT NULL,
288
+ created_at TEXT NOT NULL,
289
+ PRIMARY KEY (source, delivery_id)
290
+ );
291
+ CREATE INDEX IF NOT EXISTS source_deliveries_run_idx
292
+ ON source_deliveries(run_id);
293
+ CREATE TABLE IF NOT EXISTS control_plane_events (
294
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
295
+ type TEXT NOT NULL,
296
+ severity TEXT NOT NULL,
297
+ subject TEXT,
298
+ payload_json TEXT NOT NULL,
299
+ created_at TEXT NOT NULL
300
+ );
301
+ CREATE INDEX IF NOT EXISTS control_plane_events_type_idx
302
+ ON control_plane_events(type);
303
+ CREATE INDEX IF NOT EXISTS control_plane_events_severity_idx
304
+ ON control_plane_events(severity);
250
305
  CREATE TABLE IF NOT EXISTS suggested_changes (
251
306
  proposal_id TEXT PRIMARY KEY,
252
307
  run_id TEXT NOT NULL,
@@ -328,6 +383,7 @@ function migrateSchema(sqlite) {
328
383
  uri TEXT NOT NULL,
329
384
  body TEXT NOT NULL,
330
385
  thread_key TEXT,
386
+ idempotency_key TEXT,
331
387
  metadata_json TEXT,
332
388
  status TEXT NOT NULL,
333
389
  attempts INTEGER NOT NULL DEFAULT 0,
@@ -446,6 +502,29 @@ function migrateSchema(sqlite) {
446
502
  sqlite.exec("ALTER TABLE run_events ADD COLUMN message TEXT");
447
503
  }
448
504
  sqlite.exec("CREATE INDEX IF NOT EXISTS run_events_run_idx ON run_events(run_id)");
505
+ sqlite.exec(`
506
+ CREATE TABLE IF NOT EXISTS source_deliveries (
507
+ source TEXT NOT NULL,
508
+ delivery_id TEXT NOT NULL,
509
+ run_id TEXT NOT NULL,
510
+ event_id TEXT NOT NULL,
511
+ created_at TEXT NOT NULL,
512
+ PRIMARY KEY (source, delivery_id)
513
+ );
514
+ `);
515
+ sqlite.exec("CREATE INDEX IF NOT EXISTS source_deliveries_run_idx ON source_deliveries(run_id)");
516
+ sqlite.exec(`
517
+ CREATE TABLE IF NOT EXISTS control_plane_events (
518
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
519
+ type TEXT NOT NULL,
520
+ severity TEXT NOT NULL,
521
+ subject TEXT,
522
+ payload_json TEXT NOT NULL,
523
+ created_at TEXT NOT NULL
524
+ );
525
+ `);
526
+ sqlite.exec("CREATE INDEX IF NOT EXISTS control_plane_events_type_idx ON control_plane_events(type)");
527
+ sqlite.exec("CREATE INDEX IF NOT EXISTS control_plane_events_severity_idx ON control_plane_events(severity)");
449
528
  sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS repo_policy_rules_repo_id_idx ON repo_policy_rules(provider, owner, repo, id)");
450
529
  sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS repo_mutation_mappings_repo_id_idx ON repo_mutation_mappings(provider, owner, repo, id)");
451
530
  const callbackColumns = sqlite.prepare("PRAGMA table_info(callback_deliveries)").all();
@@ -456,6 +535,10 @@ function migrateSchema(sqlite) {
456
535
  if (!callbackColumnNames.has("metadata_json")) {
457
536
  sqlite.exec("ALTER TABLE callback_deliveries ADD COLUMN metadata_json TEXT");
458
537
  }
538
+ if (!callbackColumnNames.has("idempotency_key")) {
539
+ sqlite.exec("ALTER TABLE callback_deliveries ADD COLUMN idempotency_key TEXT");
540
+ }
541
+ sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS callback_deliveries_idempotency_key_idx ON callback_deliveries(idempotency_key)");
459
542
  const legacySlackTable = sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'slack_channel_bindings'").get();
460
543
  if (legacySlackTable) {
461
544
  sqlite.exec(`
@@ -512,8 +595,18 @@ function runFromRow(row) {
512
595
  ...result ? { result } : {}
513
596
  };
514
597
  }
598
+ function terminalRunStatus(status) {
599
+ return status === "succeeded" || status === "failed" || status === "cancelled" || status === "interrupted" || status === "timed_out";
600
+ }
601
+ function sourceContainerMetadataMatches(input) {
602
+ if (input.event.source !== input.source) return false;
603
+ return Object.entries(input.metadata).every(([key, value]) => input.event.metadata[key] === value);
604
+ }
605
+ function callbackDeliveryMetadataFromJson(metadataJson) {
606
+ return metadataJson && typeof metadataJson === "string" ? JSON.parse(metadataJson) : void 0;
607
+ }
515
608
  function callbackDeliveryFromRow(row) {
516
- const metadata = row.metadataJson && typeof row.metadataJson === "string" ? JSON.parse(row.metadataJson) : void 0;
609
+ const metadata = callbackDeliveryMetadataFromJson(row.metadataJson);
517
610
  return {
518
611
  id: row.id,
519
612
  runId: row.runId,
@@ -522,9 +615,12 @@ function callbackDeliveryFromRow(row) {
522
615
  uri: row.uri,
523
616
  body: row.body,
524
617
  ...row.threadKey ? { threadKey: row.threadKey } : {},
618
+ ...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : {},
525
619
  ...metadata?.agentId ? { agentId: metadata.agentId } : {},
526
620
  ...metadata?.statusMessageKey ? { statusMessageKey: metadata.statusMessageKey } : {},
621
+ ...metadata?.externalMessageId ? { externalMessageId: metadata.externalMessageId } : {},
527
622
  ...metadata?.blocks ? { blocks: metadata.blocks } : {},
623
+ ...metadata && "rich" in metadata ? { rich: metadata.rich } : {},
528
624
  status: row.status,
529
625
  attempts: row.attempts,
530
626
  ...row.lastError ? { lastError: row.lastError } : {},
@@ -533,6 +629,19 @@ function callbackDeliveryFromRow(row) {
533
629
  updatedAt: row.updatedAt
534
630
  };
535
631
  }
632
+ function callbackBodyHash(input) {
633
+ return createHash("sha256").update(JSON.stringify({ body: input.body, blocks: input.blocks ?? [], rich: input.rich ?? null })).digest("hex");
634
+ }
635
+ function callbackIdempotencyKey(input) {
636
+ return [
637
+ input.runId,
638
+ input.provider,
639
+ input.threadKey ?? input.uri,
640
+ input.kind,
641
+ input.statusMessageKey ?? "",
642
+ callbackBodyHash({ body: input.body, ...input.blocks ? { blocks: input.blocks } : {}, ...input.rich !== void 0 ? { rich: input.rich } : {} })
643
+ ].join("|");
644
+ }
536
645
  function followUpRequestFromRow(row) {
537
646
  return {
538
647
  id: row.id,
@@ -564,6 +673,69 @@ function recordFromJson(value) {
564
673
  return void 0;
565
674
  }
566
675
  }
676
+ function metadataString(metadata, keys) {
677
+ for (const key of keys) {
678
+ const value = metadata[key];
679
+ if (typeof value !== "string") continue;
680
+ const trimmed = value.trim();
681
+ if (trimmed.length > 0) return trimmed;
682
+ }
683
+ return null;
684
+ }
685
+ function metadataBoolean(metadata, keys) {
686
+ for (const key of keys) {
687
+ const value = metadata[key];
688
+ if (typeof value === "boolean") return value;
689
+ }
690
+ return null;
691
+ }
692
+ function signatureStateFromEvent(event) {
693
+ const explicitState = metadataString(event.metadata, ["signatureState", "webhookSignatureState"]);
694
+ if (explicitState === "verified" || explicitState === "unverified" || explicitState === "unknown") return explicitState;
695
+ const verified = metadataBoolean(event.metadata, [
696
+ "signatureVerified",
697
+ "verifiedSignature",
698
+ "webhookSignatureVerified",
699
+ "githubSignatureVerified"
700
+ ]);
701
+ if (verified === true) return "verified";
702
+ if (verified === false) return "unverified";
703
+ return "unknown";
704
+ }
705
+ function sourceDeliveryIdFromEvent(event) {
706
+ return metadataString(event.metadata, [
707
+ "sourceDeliveryId",
708
+ "webhookDeliveryId",
709
+ "deliveryId",
710
+ "githubDeliveryId",
711
+ "githubDeliveryGuid",
712
+ "slackEventId",
713
+ "larkEventId"
714
+ ]);
715
+ }
716
+ function projectTargetProvenance(ref) {
717
+ if (!ref) return null;
718
+ return {
719
+ ref: formatProjectTargetRef(ref),
720
+ ...ref
721
+ };
722
+ }
723
+ function runProvenance(input) {
724
+ return {
725
+ source: input.event.source,
726
+ sourceEventId: input.event.sourceEventId,
727
+ sourceDeliveryId: sourceDeliveryIdFromEvent(input.event),
728
+ signatureState: signatureStateFromEvent(input.event),
729
+ projectTarget: projectTargetProvenance(input.projectTarget),
730
+ admissionDecision: {
731
+ action: input.admissionDecision.action,
732
+ reasonCode: input.admissionDecision.reasonCode,
733
+ ...input.admissionDecision.eventId ? { eventId: input.admissionDecision.eventId } : {},
734
+ ...input.admissionDecision.activeRunId ? { activeRunId: input.admissionDecision.activeRunId } : {}
735
+ },
736
+ expectedRunnerId: input.expectedRunnerId
737
+ };
738
+ }
567
739
  function channelBindingFromRow(row) {
568
740
  const metadata = recordFromJson(row.metadataJson);
569
741
  return {
@@ -664,6 +836,111 @@ function emptyApplyOutcomeCounts() {
664
836
  function recordFromUnknown(value) {
665
837
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
666
838
  }
839
+ function payloadString(payload, path) {
840
+ let current = payload;
841
+ for (const segment of path) {
842
+ const record = recordFromUnknown(current);
843
+ if (!record) return null;
844
+ current = record[segment];
845
+ }
846
+ return typeof current === "string" && current.trim().length > 0 ? current : null;
847
+ }
848
+ function controlPlaneAlertSubject(event) {
849
+ if (event.type === "run.claimed") {
850
+ return payloadString(event.payload, ["runnerId"]) ?? event.subject ?? "unknown-runner";
851
+ }
852
+ if (event.type === "security.auth_failed") {
853
+ return payloadString(event.payload, ["tokenFingerprint"]) ?? event.subject ?? "unknown-token";
854
+ }
855
+ if (event.type === "security.token_misuse") {
856
+ const provider = payloadString(event.payload, ["provider"]);
857
+ const tokenKind = payloadString(event.payload, ["tokenKind"]);
858
+ if (provider && tokenKind) return `${provider}:${tokenKind}`;
859
+ return event.subject ?? "unknown-token";
860
+ }
861
+ if (event.type === "security.signature_failed") {
862
+ const provider = payloadString(event.payload, ["provider"]);
863
+ const endpoint = payloadString(event.payload, ["endpoint"]);
864
+ if (provider && endpoint) return `${provider}:${endpoint}`;
865
+ return event.subject ?? "unknown-signature-source";
866
+ }
867
+ if (event.type === "security.request_body_rejected") {
868
+ return payloadString(event.payload, ["endpoint"]) ?? event.subject ?? "unknown-endpoint";
869
+ }
870
+ if (event.type === "admission.needs_human_decision") {
871
+ const reasonCode = payloadString(event.payload, ["decision", "reasonCode"]) ?? payloadString(event.payload, ["reasonCode"]);
872
+ if (reasonCode === "repo_not_bound" || reasonCode === "repo_context_missing") {
873
+ return payloadString(event.payload, ["projectTarget"]) ?? reasonCode;
874
+ }
875
+ }
876
+ return event.subject ?? event.type;
877
+ }
878
+ function controlPlaneAlertKind(event) {
879
+ if (event.type === "run.claimed") return "abnormal_runner_claim_rate";
880
+ if (event.type === "security.auth_failed") return "repeated_auth_failures";
881
+ if (event.type === "security.token_misuse") return "token_misuse";
882
+ if (event.type === "security.signature_failed") return "repeated_signature_failures";
883
+ if (event.type === "security.request_body_rejected") {
884
+ return payloadString(event.payload, ["reason"]) === "request_body_too_large" ? "repeated_large_payload_rejections" : "repeated_invalid_request_body";
885
+ }
886
+ if (event.type === "admission.needs_human_decision") {
887
+ const reasonCode = payloadString(event.payload, ["decision", "reasonCode"]) ?? payloadString(event.payload, ["reasonCode"]);
888
+ if (reasonCode === "repo_not_bound" || reasonCode === "repo_context_missing") return "repeated_unknown_project_targets";
889
+ }
890
+ return null;
891
+ }
892
+ function controlPlaneAlertMetadata(kind) {
893
+ if (kind === "repeated_auth_failures") {
894
+ return {
895
+ severity: "warn",
896
+ reason: "Repeated dispatcher authorization failures were observed.",
897
+ nextAction: "Check for token misuse, stale runner configuration, or a leaked/rotated pairing token."
898
+ };
899
+ }
900
+ if (kind === "token_misuse") {
901
+ return {
902
+ severity: "warn",
903
+ reason: "A platform or relay token failed with a terminal authentication or configuration error.",
904
+ nextAction: "Rotate or replace the affected token, then restart or re-pair the ingress or runner that owns it."
905
+ };
906
+ }
907
+ if (kind === "repeated_large_payload_rejections") {
908
+ return {
909
+ severity: "warn",
910
+ reason: "Repeated oversized dispatcher request bodies were rejected.",
911
+ nextAction: "Check source ingress payload size, request body limits, and whether a client is retrying an invalid payload."
912
+ };
913
+ }
914
+ if (kind === "repeated_invalid_request_body") {
915
+ return {
916
+ severity: "warn",
917
+ reason: "Repeated malformed or schema-invalid request bodies were rejected.",
918
+ nextAction: "Check source webhook payload shape, client versions, and whether unsigned or incompatible traffic is hitting the endpoint."
919
+ };
920
+ }
921
+ if (kind === "repeated_signature_failures") {
922
+ return {
923
+ severity: "warn",
924
+ reason: "Repeated source webhook signature verification failures were observed.",
925
+ nextAction: "Check the source webhook secret, signing configuration, endpoint URL, and whether unsigned traffic is hitting the ingress."
926
+ };
927
+ }
928
+ if (kind === "abnormal_runner_claim_rate") {
929
+ return {
930
+ severity: "warn",
931
+ reason: "Runner claim volume exceeded the local alert threshold.",
932
+ nextAction: "Check for runaway runner loops, token misuse, or an unexpected burst of queued runs for this runner."
933
+ };
934
+ }
935
+ return {
936
+ severity: "warn",
937
+ reason: "Repeated source events resolved to missing or unbound Project Targets.",
938
+ nextAction: "Verify source metadata, Project Target bindings, and runner allowlists before retrying."
939
+ };
940
+ }
941
+ function controlPlaneAlertThreshold(kind, thresholds) {
942
+ return thresholds?.[kind] ?? (kind === "token_misuse" ? 1 : kind === "repeated_auth_failures" || kind === "repeated_signature_failures" ? 3 : kind === "abnormal_runner_claim_rate" ? 10 : 2);
943
+ }
667
944
  function metricsFromEvents(runId, events) {
668
945
  const latestApplyPlans = /* @__PURE__ */ new Map();
669
946
  for (const event of events) {
@@ -731,6 +1008,17 @@ function aggregateMetrics(input) {
731
1008
  };
732
1009
  }
733
1010
  function createOpenTagRepository(db) {
1011
+ async function repoBindingRunnerId(projectTarget) {
1012
+ if (!projectTarget) return null;
1013
+ const row = await db.select().from(repoBindings).where(
1014
+ and(
1015
+ eq(repoBindings.provider, projectTarget.provider),
1016
+ eq(repoBindings.owner, projectTarget.owner),
1017
+ eq(repoBindings.repo, projectTarget.repo)
1018
+ )
1019
+ ).limit(1).get();
1020
+ return row?.runnerId ?? null;
1021
+ }
734
1022
  async function appendRunEvent(input) {
735
1023
  await db.insert(runEvents).values({
736
1024
  runId: input.runId,
@@ -742,6 +1030,51 @@ function createOpenTagRepository(db) {
742
1030
  createdAt: input.createdAt ?? nowIso()
743
1031
  });
744
1032
  }
1033
+ async function recordCreateRunReplay(input) {
1034
+ const reason = input.replayKind === "source_delivery" ? "Source delivery already created a run." : "Source event already created a run.";
1035
+ const reasonCode = input.replayKind === "source_delivery" ? "duplicate_source_delivery" : "duplicate_source_event";
1036
+ const replayDecision = RunAdmissionDecisionSchema.parse({
1037
+ action: "drop_duplicate",
1038
+ reason,
1039
+ reasonCode,
1040
+ decidedAt: input.createdAt,
1041
+ activeRunId: input.runRow.id,
1042
+ eventId: input.event.id
1043
+ });
1044
+ await appendRunEvent({
1045
+ runId: input.runRow.id,
1046
+ type: "admission.decided",
1047
+ payload: replayDecision,
1048
+ visibility: "audit",
1049
+ importance: "normal",
1050
+ message: replayDecision.reason,
1051
+ createdAt: input.createdAt
1052
+ });
1053
+ await appendRunEvent({
1054
+ runId: input.runRow.id,
1055
+ type: "run.create_idempotent_replay",
1056
+ payload: {
1057
+ requestedRunId: input.requestedRunId,
1058
+ eventId: input.event.id,
1059
+ replayKey: input.replayKind === "source_delivery" ? { kind: "source_delivery", source: input.event.source, deliveryId: input.sourceDeliveryId } : { kind: "source_event", eventId: input.event.id },
1060
+ provenance: runProvenance({
1061
+ event: input.event,
1062
+ projectTarget: input.projectTarget,
1063
+ admissionDecision: replayDecision,
1064
+ expectedRunnerId: input.expectedRunnerId
1065
+ })
1066
+ },
1067
+ visibility: "audit",
1068
+ importance: "low",
1069
+ createdAt: input.createdAt
1070
+ });
1071
+ return {
1072
+ run: runFromRow(input.runRow),
1073
+ created: false,
1074
+ replayKind: input.replayKind,
1075
+ replayDecision
1076
+ };
1077
+ }
745
1078
  async function buildApplyPlan(input) {
746
1079
  const storedProposalRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
747
1080
  const decisionRow = await db.select().from(approvalDecisions).where(eq(approvalDecisions.id, input.approvalDecisionId)).limit(1).get();
@@ -860,6 +1193,74 @@ function createOpenTagRepository(db) {
860
1193
  event: OpenTagEventSchema.parse(JSON.parse(row.eventJson))
861
1194
  };
862
1195
  },
1196
+ async findCancelableRunForSourceContainer(input) {
1197
+ const rows = await db.select().from(runs).where(
1198
+ and(
1199
+ eq(runs.repoProvider, input.repoProvider),
1200
+ eq(runs.repoOwner, input.owner),
1201
+ eq(runs.repoName, input.repo),
1202
+ inArray(runs.status, ["queued", "assigned", "running", "needs_approval"])
1203
+ )
1204
+ ).orderBy(asc(runs.createdAt));
1205
+ for (const row of rows) {
1206
+ const event = OpenTagEventSchema.parse(JSON.parse(row.eventJson));
1207
+ if (sourceContainerMetadataMatches({ event, source: input.source, metadata: input.metadata })) {
1208
+ return { run: runFromRow(row), event };
1209
+ }
1210
+ }
1211
+ return null;
1212
+ },
1213
+ async cancelRun(input) {
1214
+ const row = await db.select().from(runs).where(eq(runs.id, input.runId)).limit(1).get();
1215
+ if (!row) return { outcome: "not_found" };
1216
+ const event = OpenTagEventSchema.parse(JSON.parse(row.eventJson));
1217
+ const existingRun = runFromRow(row);
1218
+ if (terminalRunStatus(row.status)) {
1219
+ return { outcome: "already_terminal", run: existingRun, event };
1220
+ }
1221
+ const updatedAt = nowIso();
1222
+ const result = {
1223
+ conclusion: "cancelled",
1224
+ summary: input.reason ?? "Cancellation was requested by a human.",
1225
+ nextAction: "OpenTag will not treat this stop request as a successful completion."
1226
+ };
1227
+ await db.update(runs).set({
1228
+ status: "cancelled",
1229
+ resultJson: JSON.stringify(result),
1230
+ assignedRunnerId: null,
1231
+ leasedAt: null,
1232
+ leaseExpiresAt: null,
1233
+ heartbeatAt: null,
1234
+ updatedAt
1235
+ }).where(eq(runs.id, input.runId));
1236
+ await appendRunEvent({
1237
+ runId: input.runId,
1238
+ type: "run.cancel_requested",
1239
+ payload: {
1240
+ previousStatus: row.status,
1241
+ previousRunnerId: row.assignedRunnerId,
1242
+ terminalReason: "cancelled_by_user",
1243
+ terminalSemantics: "A human stop request is not a successful completion and does not auto-promote queued follow-ups.",
1244
+ ...input.requestedBy ? { requestedBy: input.requestedBy } : {},
1245
+ reason: result.summary
1246
+ },
1247
+ visibility: "audit",
1248
+ importance: "high",
1249
+ message: result.summary,
1250
+ createdAt: updatedAt
1251
+ });
1252
+ return {
1253
+ outcome: "cancelled",
1254
+ run: {
1255
+ ...existingRun,
1256
+ status: "cancelled",
1257
+ assignedRunnerId: void 0,
1258
+ updatedAt,
1259
+ result
1260
+ },
1261
+ event
1262
+ };
1263
+ },
863
1264
  async createFollowUpRequest(input) {
864
1265
  const event = OpenTagEventSchema.parse(input.event);
865
1266
  const decision = RunAdmissionDecisionSchema.parse(input.decision);
@@ -894,6 +1295,10 @@ function createOpenTagRepository(db) {
894
1295
  const row = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.id)).limit(1).get();
895
1296
  return row ? followUpRequestFromRow(row) : null;
896
1297
  },
1298
+ async listQueuedFollowUpsForActiveRun(input) {
1299
+ const rows = await db.select().from(followUpRequests).where(and(eq(followUpRequests.activeRunId, input.activeRunId), eq(followUpRequests.status, "queued"))).orderBy(asc(followUpRequests.createdAt));
1300
+ return rows.map(followUpRequestFromRow);
1301
+ },
897
1302
  async createRunFromFollowUpRequest(input) {
898
1303
  const row = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.followUpRequestId)).limit(1).get();
899
1304
  if (!row) {
@@ -950,7 +1355,13 @@ function createOpenTagRepository(db) {
950
1355
  },
951
1356
  async registerRunner(input) {
952
1357
  const createdAt = nowIso();
953
- await db.insert(runners).values({ runnerId: input.runnerId, name: input.name, createdAt }).onConflictDoNothing();
1358
+ await db.insert(runners).values({ runnerId: input.runnerId, name: input.name, createdAt, heartbeatAt: createdAt }).onConflictDoUpdate({
1359
+ target: runners.runnerId,
1360
+ set: {
1361
+ name: input.name,
1362
+ heartbeatAt: createdAt
1363
+ }
1364
+ });
954
1365
  },
955
1366
  async getRunner(input) {
956
1367
  const row = await db.select().from(runners).where(eq(runners.runnerId, input.runnerId)).limit(1).get();
@@ -1039,6 +1450,24 @@ function createOpenTagRepository(db) {
1039
1450
  }
1040
1451
  });
1041
1452
  },
1453
+ async deleteChannelBinding(input) {
1454
+ const existing = await db.select().from(channelBindings).where(
1455
+ and(
1456
+ eq(channelBindings.provider, input.provider),
1457
+ eq(channelBindings.accountId, input.accountId),
1458
+ eq(channelBindings.conversationId, input.conversationId)
1459
+ )
1460
+ ).limit(1).get();
1461
+ if (!existing) return false;
1462
+ await db.delete(channelBindings).where(
1463
+ and(
1464
+ eq(channelBindings.provider, input.provider),
1465
+ eq(channelBindings.accountId, input.accountId),
1466
+ eq(channelBindings.conversationId, input.conversationId)
1467
+ )
1468
+ );
1469
+ return true;
1470
+ },
1042
1471
  async createSlackChannelBinding(input) {
1043
1472
  const repoProvider = input.repoProvider ?? "github";
1044
1473
  await db.insert(channelBindings).values({
@@ -1065,6 +1494,26 @@ function createOpenTagRepository(db) {
1065
1494
  const createdAt = nowIso();
1066
1495
  const protocolFields = protocolRunFieldsFromEvent(event, createdAt);
1067
1496
  const repoKey = projectTargetRefFromEvent(event);
1497
+ const expectedRunnerId = await repoBindingRunnerId(repoKey);
1498
+ const sourceDeliveryId = sourceDeliveryIdFromEvent(event);
1499
+ if (sourceDeliveryId) {
1500
+ const existingDelivery = await db.select().from(sourceDeliveries).where(and(eq(sourceDeliveries.source, event.source), eq(sourceDeliveries.deliveryId, sourceDeliveryId))).limit(1).get();
1501
+ if (existingDelivery) {
1502
+ const existingByDelivery = await db.select().from(runs).where(eq(runs.id, existingDelivery.runId)).limit(1).get();
1503
+ if (existingByDelivery) {
1504
+ return recordCreateRunReplay({
1505
+ runRow: existingByDelivery,
1506
+ requestedRunId: input.id,
1507
+ event,
1508
+ projectTarget: repoKey,
1509
+ expectedRunnerId,
1510
+ replayKind: "source_delivery",
1511
+ sourceDeliveryId,
1512
+ createdAt
1513
+ });
1514
+ }
1515
+ }
1516
+ }
1068
1517
  const insertResult = await db.insert(runs).values({
1069
1518
  id: input.id,
1070
1519
  eventId: event.id,
@@ -1088,32 +1537,16 @@ function createOpenTagRepository(db) {
1088
1537
  if (!existingBySourceEvent) {
1089
1538
  throw new Error(`Run already exists for event ${event.id}, but it could not be loaded`);
1090
1539
  }
1091
- const replayDecision = RunAdmissionDecisionSchema.parse({
1092
- action: "drop_duplicate",
1093
- reason: "Source event already created a run.",
1094
- reasonCode: "duplicate_source_event",
1095
- decidedAt: createdAt,
1096
- activeRunId: existingBySourceEvent.id,
1097
- eventId: event.id
1098
- });
1099
- await appendRunEvent({
1100
- runId: existingBySourceEvent.id,
1101
- type: "admission.decided",
1102
- payload: replayDecision,
1103
- visibility: "audit",
1104
- importance: "normal",
1105
- message: replayDecision.reason,
1106
- createdAt
1107
- });
1108
- await appendRunEvent({
1109
- runId: existingBySourceEvent.id,
1110
- type: "run.create_idempotent_replay",
1111
- payload: { requestedRunId: input.id, eventId: event.id },
1112
- visibility: "audit",
1113
- importance: "low",
1540
+ return recordCreateRunReplay({
1541
+ runRow: existingBySourceEvent,
1542
+ requestedRunId: input.id,
1543
+ event,
1544
+ projectTarget: repoKey,
1545
+ expectedRunnerId,
1546
+ replayKind: "source_event",
1547
+ sourceDeliveryId,
1114
1548
  createdAt
1115
1549
  });
1116
- return { run: runFromRow(existingBySourceEvent), created: false };
1117
1550
  }
1118
1551
  const createDecision = RunAdmissionDecisionSchema.parse({
1119
1552
  action: "start",
@@ -1122,6 +1555,15 @@ function createOpenTagRepository(db) {
1122
1555
  decidedAt: createdAt,
1123
1556
  eventId: event.id
1124
1557
  });
1558
+ if (sourceDeliveryId) {
1559
+ await db.insert(sourceDeliveries).values({
1560
+ source: event.source,
1561
+ deliveryId: sourceDeliveryId,
1562
+ runId: input.id,
1563
+ eventId: event.id,
1564
+ createdAt
1565
+ }).onConflictDoNothing({ target: [sourceDeliveries.source, sourceDeliveries.deliveryId] });
1566
+ }
1125
1567
  await appendRunEvent({
1126
1568
  runId: input.id,
1127
1569
  type: "admission.decided",
@@ -1134,7 +1576,15 @@ function createOpenTagRepository(db) {
1134
1576
  await appendRunEvent({
1135
1577
  runId: input.id,
1136
1578
  type: "run.created",
1137
- payload: { eventId: event.id },
1579
+ payload: {
1580
+ eventId: event.id,
1581
+ provenance: runProvenance({
1582
+ event,
1583
+ projectTarget: repoKey,
1584
+ admissionDecision: createDecision,
1585
+ expectedRunnerId
1586
+ })
1587
+ },
1138
1588
  visibility: "audit",
1139
1589
  importance: "low",
1140
1590
  createdAt
@@ -1184,8 +1634,35 @@ function createOpenTagRepository(db) {
1184
1634
  created: true
1185
1635
  };
1186
1636
  },
1637
+ async pruneSourceDeliveries(input) {
1638
+ const cutoff = new Date(input.olderThan);
1639
+ if (!Number.isFinite(cutoff.getTime())) {
1640
+ throw new Error("olderThan must be a valid timestamp.");
1641
+ }
1642
+ const requestedLimit = input.limit ?? 1e3;
1643
+ const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.floor(requestedLimit)) : 1e3;
1644
+ const rows = await db.select().from(sourceDeliveries).where(lt(sourceDeliveries.createdAt, cutoff.toISOString())).orderBy(asc(sourceDeliveries.createdAt)).limit(limit);
1645
+ let pruned = 0;
1646
+ let retainedActive = 0;
1647
+ for (const row of rows) {
1648
+ const runRow = await db.select({ status: runs.status }).from(runs).where(eq(runs.id, row.runId)).limit(1).get();
1649
+ if (runRow && !terminalRunStatus(runRow.status)) {
1650
+ retainedActive += 1;
1651
+ continue;
1652
+ }
1653
+ const result = await db.delete(sourceDeliveries).where(and(eq(sourceDeliveries.source, row.source), eq(sourceDeliveries.deliveryId, row.deliveryId)));
1654
+ pruned += result.changes;
1655
+ }
1656
+ return {
1657
+ scanned: rows.length,
1658
+ pruned,
1659
+ retainedActive
1660
+ };
1661
+ },
1187
1662
  async claimNextRun(input) {
1188
1663
  const now = /* @__PURE__ */ new Date();
1664
+ const runnerHeartbeatAt = nowIso();
1665
+ await db.update(runners).set({ heartbeatAt: runnerHeartbeatAt }).where(eq(runners.runnerId, input.runnerId));
1189
1666
  const activeRows = await db.select().from(runs).where(inArray(runs.status, ["assigned", "running"])).orderBy(asc(runs.createdAt));
1190
1667
  for (const activeRow of activeRows) {
1191
1668
  if (!isIsoExpired(activeRow.leaseExpiresAt, now)) continue;
@@ -1309,6 +1786,7 @@ function createOpenTagRepository(db) {
1309
1786
  if (!row) return false;
1310
1787
  const leaseSeconds = input.leaseSeconds ?? 60;
1311
1788
  const leaseExpiresAt = new Date(Date.now() + leaseSeconds * 1e3).toISOString();
1789
+ await db.update(runners).set({ heartbeatAt: updatedAt }).where(eq(runners.runnerId, input.runnerId));
1312
1790
  await db.update(runs).set({ heartbeatAt: updatedAt, leaseExpiresAt, updatedAt }).where(eq(runs.id, input.runId));
1313
1791
  await appendRunEvent({
1314
1792
  runId: input.runId,
@@ -1326,31 +1804,55 @@ function createOpenTagRepository(db) {
1326
1804
  if (input.runnerId) {
1327
1805
  conditions.push(eq(runs.assignedRunnerId, input.runnerId));
1328
1806
  }
1807
+ if (input.idempotencyKey) {
1808
+ const existing = await db.select().from(runEvents).where(eq(runEvents.runId, input.runId)).orderBy(desc(runEvents.id)).limit(250);
1809
+ for (const event of existing) {
1810
+ if (event.type !== "run.running") continue;
1811
+ const payload = recordFromJson(event.payloadJson);
1812
+ if (payload?.["idempotencyKey"] === input.idempotencyKey) return "duplicate";
1813
+ }
1814
+ }
1329
1815
  const updateResult = await db.update(runs).set({ status: "running", executor: input.executor, updatedAt }).where(and(...conditions));
1330
1816
  if (updateResult.changes === 0) {
1331
- return false;
1817
+ return "not_found";
1332
1818
  }
1333
1819
  await appendRunEvent({
1334
1820
  runId: input.runId,
1335
1821
  type: "run.running",
1336
- payload: input.runnerId ? { runnerId: input.runnerId, executor: input.executor } : { executor: input.executor },
1822
+ payload: {
1823
+ ...input.runnerId ? { runnerId: input.runnerId } : {},
1824
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1825
+ executor: input.executor,
1826
+ ...input.runTimeoutMs ? { runTimeoutMs: input.runTimeoutMs } : {}
1827
+ },
1337
1828
  visibility: "audit",
1338
1829
  importance: "normal",
1339
1830
  createdAt: updatedAt
1340
1831
  });
1341
- return true;
1832
+ return "running";
1342
1833
  },
1343
1834
  async completeRun(input) {
1344
1835
  const result = OpenTagRunResultSchema.parse(input.result);
1345
1836
  const updatedAt = nowIso();
1346
- const status = result.conclusion === "success" ? "succeeded" : result.conclusion === "cancelled" ? "cancelled" : result.conclusion === "needs_human" ? "needs_approval" : "failed";
1837
+ const status = result.conclusion === "success" ? "succeeded" : result.conclusion === "cancelled" ? "cancelled" : result.conclusion === "interrupted" ? "interrupted" : result.conclusion === "timed_out" ? "timed_out" : result.conclusion === "needs_human" ? "needs_approval" : "failed";
1347
1838
  const runRow = await db.select().from(runs).where(eq(runs.id, input.runId)).limit(1).get();
1348
1839
  if (!runRow) {
1349
- if (input.runnerId) return false;
1840
+ if (input.runnerId) return "not_found";
1350
1841
  throw new Error(`Run not found: ${input.runId}`);
1351
1842
  }
1843
+ if (input.idempotencyKey) {
1844
+ const existing = await db.select().from(runEvents).where(eq(runEvents.runId, input.runId)).orderBy(desc(runEvents.id)).limit(250);
1845
+ for (const event of existing) {
1846
+ if (event.type !== "run.completed") continue;
1847
+ const payload = recordFromJson(event.payloadJson);
1848
+ if (payload?.["idempotencyKey"] === input.idempotencyKey) return "duplicate";
1849
+ }
1850
+ }
1851
+ if (terminalRunStatus(runRow.status)) {
1852
+ return "not_found";
1853
+ }
1352
1854
  if (input.runnerId && runRow.assignedRunnerId !== input.runnerId) {
1353
- return false;
1855
+ return "not_found";
1354
1856
  }
1355
1857
  const runThread = runRow ? protocolRunFieldsFromEvent(OpenTagEventSchema.parse(JSON.parse(runRow.eventJson)), runRow.createdAt).thread : void 0;
1356
1858
  await db.update(runs).set({
@@ -1394,7 +1896,10 @@ function createOpenTagRepository(db) {
1394
1896
  await appendRunEvent({
1395
1897
  runId: input.runId,
1396
1898
  type: "run.completed",
1397
- payload: result,
1899
+ payload: {
1900
+ ...result,
1901
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}
1902
+ },
1398
1903
  visibility: "audit",
1399
1904
  importance: "high",
1400
1905
  message: result.summary,
@@ -1414,7 +1919,7 @@ function createOpenTagRepository(db) {
1414
1919
  createdAt: updatedAt
1415
1920
  });
1416
1921
  }
1417
- return true;
1922
+ return "completed";
1418
1923
  },
1419
1924
  async getSuggestedChanges(input) {
1420
1925
  const row = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
@@ -1602,13 +2107,22 @@ function createOpenTagRepository(db) {
1602
2107
  async recordProgress(input) {
1603
2108
  if (input.runnerId) {
1604
2109
  const row = await db.select().from(runs).where(and(eq(runs.id, input.runId), eq(runs.assignedRunnerId, input.runnerId))).limit(1).get();
1605
- if (!row) return false;
2110
+ if (!row) return "not_found";
2111
+ }
2112
+ if (input.idempotencyKey) {
2113
+ const existing = await db.select().from(runEvents).where(eq(runEvents.runId, input.runId)).orderBy(desc(runEvents.id)).limit(250);
2114
+ for (const event of existing) {
2115
+ if (event.type !== "run.progress") continue;
2116
+ const payload = recordFromJson(event.payloadJson);
2117
+ if (payload?.["idempotencyKey"] === input.idempotencyKey) return "duplicate";
2118
+ }
1606
2119
  }
1607
2120
  await appendRunEvent({
1608
2121
  runId: input.runId,
1609
2122
  type: "run.progress",
1610
2123
  payload: {
1611
2124
  ...input.runnerId ? { runnerId: input.runnerId } : {},
2125
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1612
2126
  type: input.type ?? "progress",
1613
2127
  message: input.message,
1614
2128
  at: input.at ?? nowIso()
@@ -1618,7 +2132,7 @@ function createOpenTagRepository(db) {
1618
2132
  message: input.message,
1619
2133
  createdAt: input.at ?? nowIso()
1620
2134
  });
1621
- return true;
2135
+ return "recorded";
1622
2136
  },
1623
2137
  async getRun(input) {
1624
2138
  const row = await db.select().from(runs).where(eq(runs.id, input.runId)).limit(1).get();
@@ -1641,8 +2155,91 @@ function createOpenTagRepository(db) {
1641
2155
  createdAt: row.createdAt
1642
2156
  }));
1643
2157
  },
2158
+ async appendControlPlaneEvent(input) {
2159
+ await db.insert(controlPlaneEvents).values({
2160
+ type: input.type,
2161
+ severity: input.severity ?? "info",
2162
+ subject: input.subject ?? null,
2163
+ payloadJson: JSON.stringify(input.payload ?? {}),
2164
+ createdAt: input.createdAt ?? nowIso()
2165
+ });
2166
+ },
2167
+ async listControlPlaneEvents(input = {}) {
2168
+ const conditions = [
2169
+ ...input.type ? [eq(controlPlaneEvents.type, input.type)] : [],
2170
+ ...input.severity ? [eq(controlPlaneEvents.severity, input.severity)] : []
2171
+ ];
2172
+ const rows = await db.select().from(controlPlaneEvents).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(asc(controlPlaneEvents.id)).limit(input.limit ?? 100);
2173
+ return rows.map((row) => ({
2174
+ id: row.id,
2175
+ type: row.type,
2176
+ severity: row.severity,
2177
+ ...row.subject ? { subject: row.subject } : {},
2178
+ payload: JSON.parse(row.payloadJson),
2179
+ createdAt: row.createdAt
2180
+ }));
2181
+ },
2182
+ async summarizeControlPlaneAlerts(input = {}) {
2183
+ const limit = input.limit ?? 5e3;
2184
+ const rows = await db.select().from(controlPlaneEvents).orderBy(desc(controlPlaneEvents.id)).limit(limit);
2185
+ const claimRows = await db.select().from(runEvents).where(eq(runEvents.type, "run.claimed")).orderBy(desc(runEvents.id)).limit(limit);
2186
+ const groups = /* @__PURE__ */ new Map();
2187
+ function addEvent(event) {
2188
+ if (input.since && event.createdAt < input.since) return;
2189
+ const kind = controlPlaneAlertKind(event);
2190
+ if (!kind) return;
2191
+ const subject = controlPlaneAlertSubject(event);
2192
+ const key = `${kind}|${event.type}|${subject}`;
2193
+ const group = groups.get(key) ?? { kind, eventType: event.type, subject, events: [] };
2194
+ group.events.push(event);
2195
+ groups.set(key, group);
2196
+ }
2197
+ for (const row of rows.reverse()) {
2198
+ addEvent({
2199
+ id: row.id,
2200
+ type: row.type,
2201
+ severity: row.severity,
2202
+ ...row.subject ? { subject: row.subject } : {},
2203
+ payload: JSON.parse(row.payloadJson),
2204
+ createdAt: row.createdAt
2205
+ });
2206
+ }
2207
+ for (const row of claimRows.reverse()) {
2208
+ addEvent({
2209
+ id: row.id,
2210
+ type: row.type,
2211
+ severity: "info",
2212
+ subject: row.runId,
2213
+ payload: JSON.parse(row.payloadJson),
2214
+ createdAt: row.createdAt
2215
+ });
2216
+ }
2217
+ return [...groups.values()].flatMap((group) => {
2218
+ const threshold = controlPlaneAlertThreshold(group.kind, input.thresholds);
2219
+ if (group.events.length < threshold) return [];
2220
+ const metadata = controlPlaneAlertMetadata(group.kind);
2221
+ const first = group.events[0];
2222
+ const last = group.events.at(-1);
2223
+ return [
2224
+ {
2225
+ id: `${group.kind}:${group.eventType}:${group.subject}`,
2226
+ type: group.kind,
2227
+ severity: metadata.severity,
2228
+ eventType: group.eventType,
2229
+ count: group.events.length,
2230
+ threshold,
2231
+ firstSeenAt: first.createdAt,
2232
+ lastSeenAt: last.createdAt,
2233
+ subject: group.subject,
2234
+ reason: metadata.reason,
2235
+ nextAction: metadata.nextAction
2236
+ }
2237
+ ];
2238
+ }).sort((left, right) => right.count - left.count || left.id.localeCompare(right.id));
2239
+ },
1644
2240
  async enqueueCallbackDelivery(input) {
1645
2241
  const createdAt = nowIso();
2242
+ const idempotencyKey = callbackIdempotencyKey(input);
1646
2243
  const rows = await db.insert(callbackDeliveries).values({
1647
2244
  runId: input.runId,
1648
2245
  kind: input.kind,
@@ -1650,17 +2247,32 @@ function createOpenTagRepository(db) {
1650
2247
  uri: input.uri,
1651
2248
  body: input.body,
1652
2249
  threadKey: input.threadKey ?? null,
2250
+ idempotencyKey,
1653
2251
  metadataJson: JSON.stringify({
1654
2252
  ...input.agentId ? { agentId: input.agentId } : {},
1655
2253
  ...input.statusMessageKey ? { statusMessageKey: input.statusMessageKey } : {},
1656
- ...input.blocks ? { blocks: input.blocks } : {}
2254
+ ...input.blocks ? { blocks: input.blocks } : {},
2255
+ ...input.rich !== void 0 ? { rich: input.rich } : {}
1657
2256
  }),
1658
2257
  status: "pending",
1659
2258
  createdAt,
1660
2259
  updatedAt: createdAt
1661
- }).returning();
2260
+ }).onConflictDoNothing({ target: callbackDeliveries.idempotencyKey }).returning();
1662
2261
  const row = rows[0];
1663
- if (!row) throw new Error("callback delivery was not created");
2262
+ if (!row) {
2263
+ const existing = await db.select().from(callbackDeliveries).where(eq(callbackDeliveries.idempotencyKey, idempotencyKey)).limit(1).get();
2264
+ if (!existing) throw new Error("callback delivery was not created");
2265
+ await appendRunEvent({
2266
+ runId: input.runId,
2267
+ type: `callback.${input.kind}.duplicate`,
2268
+ payload: callbackDeliveryFromRow(existing),
2269
+ visibility: "audit",
2270
+ importance: "normal",
2271
+ message: "Duplicate callback delivery suppressed.",
2272
+ createdAt
2273
+ });
2274
+ return callbackDeliveryFromRow(existing);
2275
+ }
1664
2276
  await appendRunEvent({
1665
2277
  runId: input.runId,
1666
2278
  type: `callback.${input.kind}.queued`,
@@ -1675,29 +2287,46 @@ function createOpenTagRepository(db) {
1675
2287
  const updatedAt = nowIso();
1676
2288
  const row = await db.select().from(callbackDeliveries).where(eq(callbackDeliveries.id, input.deliveryId)).limit(1).get();
1677
2289
  if (!row) return;
1678
- await db.update(callbackDeliveries).set({ status: "delivered", attempts: row.attempts + 1, lastError: null, nextAttemptAt: null, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
2290
+ const metadata = callbackDeliveryMetadataFromJson(row.metadataJson) ?? {};
2291
+ const metadataJson = JSON.stringify({
2292
+ ...metadata,
2293
+ ...input.externalMessageId ? { externalMessageId: input.externalMessageId } : {}
2294
+ });
2295
+ await db.update(callbackDeliveries).set({ status: "delivered", attempts: row.attempts + 1, lastError: null, nextAttemptAt: null, metadataJson, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
2296
+ const deliveredRow = { ...row, metadataJson };
1679
2297
  await appendRunEvent({
1680
2298
  runId: row.runId,
1681
2299
  type: `callback.${row.kind}.delivered`,
1682
- payload: { ...callbackDeliveryFromRow(row), status: "delivered", attempts: row.attempts + 1, updatedAt },
2300
+ payload: { ...callbackDeliveryFromRow(deliveredRow), status: "delivered", attempts: row.attempts + 1, updatedAt },
1683
2301
  visibility: "human",
1684
2302
  importance: row.kind === "final" ? "high" : "normal",
1685
2303
  message: row.body,
1686
2304
  createdAt: updatedAt
1687
2305
  });
1688
2306
  },
2307
+ async findCallbackExternalMessageId(input) {
2308
+ const rows = await db.select().from(callbackDeliveries).where(and(eq(callbackDeliveries.runId, input.runId), eq(callbackDeliveries.provider, input.provider), eq(callbackDeliveries.status, "delivered"))).orderBy(desc(callbackDeliveries.updatedAt), desc(callbackDeliveries.id));
2309
+ for (const row of rows) {
2310
+ const delivery = callbackDeliveryFromRow(row);
2311
+ if (delivery.statusMessageKey !== input.statusMessageKey) continue;
2312
+ if ((delivery.threadKey ?? void 0) !== input.threadKey) continue;
2313
+ if (delivery.externalMessageId) return delivery.externalMessageId;
2314
+ }
2315
+ return void 0;
2316
+ },
1689
2317
  async markCallbackFailed(input) {
1690
2318
  const updatedAt = nowIso();
1691
2319
  const row = await db.select().from(callbackDeliveries).where(eq(callbackDeliveries.id, input.deliveryId)).limit(1).get();
1692
2320
  if (!row) return;
1693
- await db.update(callbackDeliveries).set({ status: "failed", attempts: row.attempts + 1, lastError: input.error, nextAttemptAt: input.nextAttemptAt ?? null, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
2321
+ const attempts = row.attempts + 1;
2322
+ await db.update(callbackDeliveries).set({ status: "failed", attempts, lastError: input.error, nextAttemptAt: input.nextAttemptAt ?? null, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
1694
2323
  await appendRunEvent({
1695
2324
  runId: row.runId,
1696
2325
  type: `callback.${row.kind}.failed`,
1697
2326
  payload: {
1698
2327
  ...callbackDeliveryFromRow(row),
1699
2328
  status: "failed",
1700
- attempts: row.attempts + 1,
2329
+ attempts,
1701
2330
  lastError: input.error,
1702
2331
  ...input.nextAttemptAt ? { nextAttemptAt: input.nextAttemptAt } : {},
1703
2332
  updatedAt
@@ -1706,6 +2335,24 @@ function createOpenTagRepository(db) {
1706
2335
  importance: "normal",
1707
2336
  createdAt: updatedAt
1708
2337
  });
2338
+ if (input.maxAttempts !== void 0 && attempts >= input.maxAttempts && !input.nextAttemptAt) {
2339
+ await appendRunEvent({
2340
+ runId: row.runId,
2341
+ type: `callback.${row.kind}.suppressed`,
2342
+ payload: {
2343
+ ...callbackDeliveryFromRow(row),
2344
+ status: "failed",
2345
+ attempts,
2346
+ maxAttempts: input.maxAttempts,
2347
+ lastError: input.error,
2348
+ updatedAt
2349
+ },
2350
+ visibility: "audit",
2351
+ importance: "high",
2352
+ message: "Callback delivery retry budget exhausted; further delivery attempts are suppressed to avoid duplicate storms.",
2353
+ createdAt: updatedAt
2354
+ });
2355
+ }
1709
2356
  },
1710
2357
  async listPendingCallbackDeliveries(input) {
1711
2358
  const now = input.now ?? /* @__PURE__ */ new Date();
@@ -1815,6 +2462,7 @@ export {
1815
2462
  approvalDecisions,
1816
2463
  callbackDeliveries,
1817
2464
  channelBindings,
2465
+ controlPlaneEvents,
1818
2466
  createOpenTagRepository,
1819
2467
  followUpRequests,
1820
2468
  migrateSchema,
@@ -1824,6 +2472,7 @@ export {
1824
2472
  runEvents,
1825
2473
  runners,
1826
2474
  runs,
2475
+ sourceDeliveries,
1827
2476
  suggestedChanges
1828
2477
  };
1829
2478
  //# sourceMappingURL=index.js.map