@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 +695 -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,
|
|
@@ -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 =
|
|
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 }).
|
|
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
|
-
|
|
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",
|
|
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: {
|
|
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
|
|
1817
|
+
return "not_found";
|
|
1332
1818
|
}
|
|
1333
1819
|
await appendRunEvent({
|
|
1334
1820
|
runId: input.runId,
|
|
1335
1821
|
type: "run.running",
|
|
1336
|
-
payload:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|