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