@opentag/store 0.1.0 → 0.3.0
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/index.js +1463 -54
- package/dist/index.js.map +1 -1
- package/dist/repository.d.ts +269 -5
- package/dist/repository.d.ts.map +1 -1
- package/dist/schema.d.ts +1399 -119
- package/dist/schema.d.ts.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
// src/repository.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
ApprovalDecisionSchema,
|
|
4
|
+
ApplyIntentOutcomeSchema,
|
|
5
|
+
ApplyPlanSchema,
|
|
6
|
+
ActionHintSchema,
|
|
7
|
+
AdapterMutationMappingSchema,
|
|
8
|
+
ContextPacketSchema,
|
|
9
|
+
conversationKeyFromEvent,
|
|
10
|
+
defaultRunEventMetadata,
|
|
11
|
+
OpenTagEventSchema,
|
|
12
|
+
OpenTagRunResultSchema,
|
|
13
|
+
PolicyRuleSchema,
|
|
14
|
+
ProposalLineageSchema,
|
|
15
|
+
preflightMutationIntent,
|
|
16
|
+
projectTargetRefFromEvent,
|
|
17
|
+
protocolRunFieldsFromEvent,
|
|
18
|
+
RunAdmissionDecisionSchema,
|
|
19
|
+
RunEventImportanceSchema,
|
|
20
|
+
RunEventVisibilitySchema,
|
|
21
|
+
SuggestedChangesSnapshotSchema
|
|
22
|
+
} from "@opentag/core";
|
|
23
|
+
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
4
24
|
|
|
5
25
|
// src/schema.ts
|
|
6
|
-
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
26
|
+
import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
7
27
|
var runs = sqliteTable(
|
|
8
28
|
"runs",
|
|
9
29
|
{
|
|
@@ -11,9 +31,19 @@ var runs = sqliteTable(
|
|
|
11
31
|
eventId: text("event_id").notNull(),
|
|
12
32
|
status: text("status").notNull(),
|
|
13
33
|
eventJson: text("event_json").notNull(),
|
|
34
|
+
contextPacketJson: text("context_packet_json"),
|
|
14
35
|
resultJson: text("result_json"),
|
|
15
36
|
assignedRunnerId: text("assigned_runner_id"),
|
|
16
37
|
executor: text("executor"),
|
|
38
|
+
parentRunId: text("parent_run_id"),
|
|
39
|
+
triggeredByActionJson: text("triggered_by_action_json"),
|
|
40
|
+
sourceProposalId: text("source_proposal_id"),
|
|
41
|
+
sourceApplyPlanId: text("source_apply_plan_id"),
|
|
42
|
+
repoProvider: text("repo_provider"),
|
|
43
|
+
repoOwner: text("repo_owner"),
|
|
44
|
+
repoName: text("repo_name"),
|
|
45
|
+
workThreadId: text("work_thread_id"),
|
|
46
|
+
conversationKey: text("conversation_key"),
|
|
17
47
|
leasedAt: text("leased_at"),
|
|
18
48
|
leaseExpiresAt: text("lease_expires_at"),
|
|
19
49
|
heartbeatAt: text("heartbeat_at"),
|
|
@@ -22,14 +52,64 @@ var runs = sqliteTable(
|
|
|
22
52
|
},
|
|
23
53
|
(table) => ({
|
|
24
54
|
statusIdx: index("runs_status_idx").on(table.status),
|
|
25
|
-
runnerIdx: index("runs_runner_idx").on(table.assignedRunnerId)
|
|
55
|
+
runnerIdx: index("runs_runner_idx").on(table.assignedRunnerId),
|
|
56
|
+
repoIdx: index("runs_repo_idx").on(table.repoProvider, table.repoOwner, table.repoName),
|
|
57
|
+
workThreadIdx: index("runs_work_thread_idx").on(table.workThreadId),
|
|
58
|
+
conversationIdx: index("runs_conversation_idx").on(table.conversationKey)
|
|
26
59
|
})
|
|
27
60
|
);
|
|
28
|
-
var
|
|
29
|
-
|
|
61
|
+
var followUpRequests = sqliteTable(
|
|
62
|
+
"follow_up_requests",
|
|
63
|
+
{
|
|
64
|
+
id: text("id").primaryKey(),
|
|
65
|
+
sourceEventId: text("source_event_id").notNull(),
|
|
66
|
+
conversationKey: text("conversation_key").notNull(),
|
|
67
|
+
activeRunId: text("active_run_id"),
|
|
68
|
+
eventJson: text("event_json").notNull(),
|
|
69
|
+
decisionJson: text("decision_json").notNull(),
|
|
70
|
+
status: text("status").notNull(),
|
|
71
|
+
createdRunId: text("created_run_id"),
|
|
72
|
+
createdAt: text("created_at").notNull(),
|
|
73
|
+
updatedAt: text("updated_at").notNull()
|
|
74
|
+
},
|
|
75
|
+
(table) => ({
|
|
76
|
+
sourceEventIdx: uniqueIndex("follow_up_requests_source_event_idx").on(table.sourceEventId),
|
|
77
|
+
conversationIdx: index("follow_up_requests_conversation_idx").on(table.conversationKey, table.status)
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
var runEvents = sqliteTable(
|
|
81
|
+
"run_events",
|
|
82
|
+
{
|
|
83
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
84
|
+
runId: text("run_id").notNull(),
|
|
85
|
+
type: text("type").notNull(),
|
|
86
|
+
visibility: text("visibility").notNull().default("audit"),
|
|
87
|
+
importance: text("importance").notNull().default("normal"),
|
|
88
|
+
message: text("message"),
|
|
89
|
+
payloadJson: text("payload_json").notNull(),
|
|
90
|
+
createdAt: text("created_at").notNull()
|
|
91
|
+
},
|
|
92
|
+
(table) => ({
|
|
93
|
+
runIdx: index("run_events_run_idx").on(table.runId)
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
var suggestedChanges = sqliteTable("suggested_changes", {
|
|
97
|
+
proposalId: text("proposal_id").primaryKey(),
|
|
30
98
|
runId: text("run_id").notNull(),
|
|
31
|
-
|
|
32
|
-
|
|
99
|
+
snapshotJson: text("snapshot_json").notNull(),
|
|
100
|
+
createdAt: text("created_at").notNull()
|
|
101
|
+
});
|
|
102
|
+
var approvalDecisions = sqliteTable("approval_decisions", {
|
|
103
|
+
id: text("id").primaryKey(),
|
|
104
|
+
proposalId: text("proposal_id").notNull(),
|
|
105
|
+
decisionJson: text("decision_json").notNull(),
|
|
106
|
+
createdAt: text("created_at").notNull()
|
|
107
|
+
});
|
|
108
|
+
var applyPlans = sqliteTable("apply_plans", {
|
|
109
|
+
id: text("id").primaryKey(),
|
|
110
|
+
proposalId: text("proposal_id").notNull(),
|
|
111
|
+
approvalDecisionId: text("approval_decision_id").notNull(),
|
|
112
|
+
planJson: text("plan_json").notNull(),
|
|
33
113
|
createdAt: text("created_at").notNull()
|
|
34
114
|
});
|
|
35
115
|
var runners = sqliteTable("runners", {
|
|
@@ -55,18 +135,76 @@ var repoBindings = sqliteTable(
|
|
|
55
135
|
repoUniqueIdx: uniqueIndex("repo_bindings_provider_owner_repo_idx").on(table.provider, table.owner, table.repo)
|
|
56
136
|
})
|
|
57
137
|
);
|
|
58
|
-
var
|
|
59
|
-
"
|
|
138
|
+
var repoPolicyRules = sqliteTable(
|
|
139
|
+
"repo_policy_rules",
|
|
140
|
+
{
|
|
141
|
+
id: text("id").notNull(),
|
|
142
|
+
provider: text("provider").notNull(),
|
|
143
|
+
owner: text("owner").notNull(),
|
|
144
|
+
repo: text("repo").notNull(),
|
|
145
|
+
ruleJson: text("rule_json").notNull(),
|
|
146
|
+
createdAt: text("created_at").notNull()
|
|
147
|
+
},
|
|
148
|
+
(table) => ({
|
|
149
|
+
pk: primaryKey({ columns: [table.provider, table.owner, table.repo, table.id] })
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
var repoMutationMappings = sqliteTable(
|
|
153
|
+
"repo_mutation_mappings",
|
|
154
|
+
{
|
|
155
|
+
id: text("id").notNull(),
|
|
156
|
+
provider: text("provider").notNull(),
|
|
157
|
+
owner: text("owner").notNull(),
|
|
158
|
+
repo: text("repo").notNull(),
|
|
159
|
+
mappingJson: text("mapping_json").notNull(),
|
|
160
|
+
createdAt: text("created_at").notNull()
|
|
161
|
+
},
|
|
162
|
+
(table) => ({
|
|
163
|
+
pk: primaryKey({ columns: [table.provider, table.owner, table.repo, table.id] })
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
var channelBindings = sqliteTable(
|
|
167
|
+
"channel_bindings",
|
|
60
168
|
{
|
|
61
169
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
62
|
-
|
|
63
|
-
|
|
170
|
+
provider: text("provider").notNull(),
|
|
171
|
+
accountId: text("account_id").notNull(),
|
|
172
|
+
conversationId: text("conversation_id").notNull(),
|
|
173
|
+
repoProvider: text("repo_provider").notNull(),
|
|
64
174
|
owner: text("owner").notNull(),
|
|
65
175
|
repo: text("repo").notNull(),
|
|
176
|
+
metadataJson: text("metadata_json"),
|
|
66
177
|
createdAt: text("created_at").notNull()
|
|
67
178
|
},
|
|
68
179
|
(table) => ({
|
|
69
|
-
|
|
180
|
+
channelBindingUniqueIdx: uniqueIndex("channel_bindings_provider_account_conversation_idx").on(
|
|
181
|
+
table.provider,
|
|
182
|
+
table.accountId,
|
|
183
|
+
table.conversationId
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
var callbackDeliveries = sqliteTable(
|
|
188
|
+
"callback_deliveries",
|
|
189
|
+
{
|
|
190
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
191
|
+
runId: text("run_id").notNull(),
|
|
192
|
+
kind: text("kind").notNull(),
|
|
193
|
+
provider: text("provider").notNull(),
|
|
194
|
+
uri: text("uri").notNull(),
|
|
195
|
+
body: text("body").notNull(),
|
|
196
|
+
threadKey: text("thread_key"),
|
|
197
|
+
metadataJson: text("metadata_json"),
|
|
198
|
+
status: text("status").notNull(),
|
|
199
|
+
attempts: integer("attempts").notNull().default(0),
|
|
200
|
+
lastError: text("last_error"),
|
|
201
|
+
nextAttemptAt: text("next_attempt_at"),
|
|
202
|
+
createdAt: text("created_at").notNull(),
|
|
203
|
+
updatedAt: text("updated_at").notNull()
|
|
204
|
+
},
|
|
205
|
+
(table) => ({
|
|
206
|
+
callbackRunIdx: index("callback_deliveries_run_idx").on(table.runId),
|
|
207
|
+
callbackStatusIdx: index("callback_deliveries_status_idx").on(table.status)
|
|
70
208
|
})
|
|
71
209
|
);
|
|
72
210
|
function migrateSchema(sqlite) {
|
|
@@ -76,9 +214,19 @@ function migrateSchema(sqlite) {
|
|
|
76
214
|
event_id TEXT NOT NULL,
|
|
77
215
|
status TEXT NOT NULL,
|
|
78
216
|
event_json TEXT NOT NULL,
|
|
217
|
+
context_packet_json TEXT,
|
|
79
218
|
result_json TEXT,
|
|
80
219
|
assigned_runner_id TEXT,
|
|
81
220
|
executor TEXT,
|
|
221
|
+
parent_run_id TEXT,
|
|
222
|
+
triggered_by_action_json TEXT,
|
|
223
|
+
source_proposal_id TEXT,
|
|
224
|
+
source_apply_plan_id TEXT,
|
|
225
|
+
repo_provider TEXT,
|
|
226
|
+
repo_owner TEXT,
|
|
227
|
+
repo_name TEXT,
|
|
228
|
+
work_thread_id TEXT,
|
|
229
|
+
conversation_key TEXT,
|
|
82
230
|
leased_at TEXT,
|
|
83
231
|
lease_expires_at TEXT,
|
|
84
232
|
heartbeat_at TEXT,
|
|
@@ -87,13 +235,37 @@ function migrateSchema(sqlite) {
|
|
|
87
235
|
);
|
|
88
236
|
CREATE INDEX IF NOT EXISTS runs_status_idx ON runs(status);
|
|
89
237
|
CREATE INDEX IF NOT EXISTS runs_runner_idx ON runs(assigned_runner_id);
|
|
238
|
+
CREATE INDEX IF NOT EXISTS runs_conversation_idx ON runs(conversation_key);
|
|
90
239
|
CREATE TABLE IF NOT EXISTS run_events (
|
|
91
240
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
241
|
run_id TEXT NOT NULL,
|
|
93
242
|
type TEXT NOT NULL,
|
|
243
|
+
visibility TEXT NOT NULL DEFAULT 'audit',
|
|
244
|
+
importance TEXT NOT NULL DEFAULT 'normal',
|
|
245
|
+
message TEXT,
|
|
94
246
|
payload_json TEXT NOT NULL,
|
|
95
247
|
created_at TEXT NOT NULL
|
|
96
248
|
);
|
|
249
|
+
CREATE INDEX IF NOT EXISTS run_events_run_idx ON run_events(run_id);
|
|
250
|
+
CREATE TABLE IF NOT EXISTS suggested_changes (
|
|
251
|
+
proposal_id TEXT PRIMARY KEY,
|
|
252
|
+
run_id TEXT NOT NULL,
|
|
253
|
+
snapshot_json TEXT NOT NULL,
|
|
254
|
+
created_at TEXT NOT NULL
|
|
255
|
+
);
|
|
256
|
+
CREATE TABLE IF NOT EXISTS approval_decisions (
|
|
257
|
+
id TEXT PRIMARY KEY,
|
|
258
|
+
proposal_id TEXT NOT NULL,
|
|
259
|
+
decision_json TEXT NOT NULL,
|
|
260
|
+
created_at TEXT NOT NULL
|
|
261
|
+
);
|
|
262
|
+
CREATE TABLE IF NOT EXISTS apply_plans (
|
|
263
|
+
id TEXT PRIMARY KEY,
|
|
264
|
+
proposal_id TEXT NOT NULL,
|
|
265
|
+
approval_decision_id TEXT NOT NULL,
|
|
266
|
+
plan_json TEXT NOT NULL,
|
|
267
|
+
created_at TEXT NOT NULL
|
|
268
|
+
);
|
|
97
269
|
CREATE TABLE IF NOT EXISTS runners (
|
|
98
270
|
runner_id TEXT PRIMARY KEY,
|
|
99
271
|
name TEXT NOT NULL,
|
|
@@ -113,16 +285,77 @@ function migrateSchema(sqlite) {
|
|
|
113
285
|
);
|
|
114
286
|
CREATE UNIQUE INDEX IF NOT EXISTS repo_bindings_provider_owner_repo_idx
|
|
115
287
|
ON repo_bindings(provider, owner, repo);
|
|
116
|
-
CREATE TABLE IF NOT EXISTS
|
|
288
|
+
CREATE TABLE IF NOT EXISTS repo_policy_rules (
|
|
289
|
+
id TEXT NOT NULL,
|
|
290
|
+
provider TEXT NOT NULL,
|
|
291
|
+
owner TEXT NOT NULL,
|
|
292
|
+
repo TEXT NOT NULL,
|
|
293
|
+
rule_json TEXT NOT NULL,
|
|
294
|
+
created_at TEXT NOT NULL,
|
|
295
|
+
PRIMARY KEY (provider, owner, repo, id)
|
|
296
|
+
);
|
|
297
|
+
CREATE UNIQUE INDEX IF NOT EXISTS repo_policy_rules_repo_id_idx
|
|
298
|
+
ON repo_policy_rules(provider, owner, repo, id);
|
|
299
|
+
CREATE TABLE IF NOT EXISTS repo_mutation_mappings (
|
|
300
|
+
id TEXT NOT NULL,
|
|
301
|
+
provider TEXT NOT NULL,
|
|
302
|
+
owner TEXT NOT NULL,
|
|
303
|
+
repo TEXT NOT NULL,
|
|
304
|
+
mapping_json TEXT NOT NULL,
|
|
305
|
+
created_at TEXT NOT NULL,
|
|
306
|
+
PRIMARY KEY (provider, owner, repo, id)
|
|
307
|
+
);
|
|
308
|
+
CREATE UNIQUE INDEX IF NOT EXISTS repo_mutation_mappings_repo_id_idx
|
|
309
|
+
ON repo_mutation_mappings(provider, owner, repo, id);
|
|
310
|
+
CREATE TABLE IF NOT EXISTS channel_bindings (
|
|
117
311
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
-
|
|
119
|
-
|
|
312
|
+
provider TEXT NOT NULL,
|
|
313
|
+
account_id TEXT NOT NULL,
|
|
314
|
+
conversation_id TEXT NOT NULL,
|
|
315
|
+
repo_provider TEXT NOT NULL,
|
|
120
316
|
owner TEXT NOT NULL,
|
|
121
317
|
repo TEXT NOT NULL,
|
|
318
|
+
metadata_json TEXT,
|
|
122
319
|
created_at TEXT NOT NULL
|
|
123
320
|
);
|
|
124
|
-
CREATE UNIQUE INDEX IF NOT EXISTS
|
|
125
|
-
ON
|
|
321
|
+
CREATE UNIQUE INDEX IF NOT EXISTS channel_bindings_provider_account_conversation_idx
|
|
322
|
+
ON channel_bindings(provider, account_id, conversation_id);
|
|
323
|
+
CREATE TABLE IF NOT EXISTS callback_deliveries (
|
|
324
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
325
|
+
run_id TEXT NOT NULL,
|
|
326
|
+
kind TEXT NOT NULL,
|
|
327
|
+
provider TEXT NOT NULL,
|
|
328
|
+
uri TEXT NOT NULL,
|
|
329
|
+
body TEXT NOT NULL,
|
|
330
|
+
thread_key TEXT,
|
|
331
|
+
metadata_json TEXT,
|
|
332
|
+
status TEXT NOT NULL,
|
|
333
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
334
|
+
last_error TEXT,
|
|
335
|
+
next_attempt_at TEXT,
|
|
336
|
+
created_at TEXT NOT NULL,
|
|
337
|
+
updated_at TEXT NOT NULL
|
|
338
|
+
);
|
|
339
|
+
CREATE INDEX IF NOT EXISTS callback_deliveries_run_idx
|
|
340
|
+
ON callback_deliveries(run_id);
|
|
341
|
+
CREATE INDEX IF NOT EXISTS callback_deliveries_status_idx
|
|
342
|
+
ON callback_deliveries(status);
|
|
343
|
+
CREATE TABLE IF NOT EXISTS follow_up_requests (
|
|
344
|
+
id TEXT PRIMARY KEY,
|
|
345
|
+
source_event_id TEXT NOT NULL,
|
|
346
|
+
conversation_key TEXT NOT NULL,
|
|
347
|
+
active_run_id TEXT,
|
|
348
|
+
event_json TEXT NOT NULL,
|
|
349
|
+
decision_json TEXT NOT NULL,
|
|
350
|
+
status TEXT NOT NULL,
|
|
351
|
+
created_run_id TEXT,
|
|
352
|
+
created_at TEXT NOT NULL,
|
|
353
|
+
updated_at TEXT NOT NULL
|
|
354
|
+
);
|
|
355
|
+
CREATE UNIQUE INDEX IF NOT EXISTS follow_up_requests_source_event_idx
|
|
356
|
+
ON follow_up_requests(source_event_id);
|
|
357
|
+
CREATE INDEX IF NOT EXISTS follow_up_requests_conversation_idx
|
|
358
|
+
ON follow_up_requests(conversation_key, status);
|
|
126
359
|
`);
|
|
127
360
|
const columns = sqlite.prepare("PRAGMA table_info(repo_bindings)").all();
|
|
128
361
|
const columnNames = new Set(columns.map((column) => column.name));
|
|
@@ -135,14 +368,117 @@ function migrateSchema(sqlite) {
|
|
|
135
368
|
if (!columnNames.has("allowed_actors_json")) {
|
|
136
369
|
sqlite.exec("ALTER TABLE repo_bindings ADD COLUMN allowed_actors_json TEXT");
|
|
137
370
|
}
|
|
371
|
+
const channelBindingColumns = sqlite.prepare("PRAGMA table_info(channel_bindings)").all();
|
|
372
|
+
const channelBindingColumnNames = new Set(channelBindingColumns.map((column) => column.name));
|
|
373
|
+
if (!channelBindingColumnNames.has("repo_provider")) {
|
|
374
|
+
sqlite.exec("ALTER TABLE channel_bindings ADD COLUMN repo_provider TEXT");
|
|
375
|
+
sqlite.exec("UPDATE channel_bindings SET repo_provider = 'github' WHERE repo_provider IS NULL");
|
|
376
|
+
}
|
|
377
|
+
if (!channelBindingColumnNames.has("metadata_json")) {
|
|
378
|
+
sqlite.exec("ALTER TABLE channel_bindings ADD COLUMN metadata_json TEXT");
|
|
379
|
+
}
|
|
138
380
|
const runColumns = sqlite.prepare("PRAGMA table_info(runs)").all();
|
|
139
381
|
const runColumnNames = new Set(runColumns.map((column) => column.name));
|
|
140
382
|
if (!runColumnNames.has("leased_at")) {
|
|
141
383
|
sqlite.exec("ALTER TABLE runs ADD COLUMN leased_at TEXT");
|
|
142
384
|
}
|
|
385
|
+
if (!runColumnNames.has("context_packet_json")) {
|
|
386
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN context_packet_json TEXT");
|
|
387
|
+
}
|
|
143
388
|
if (!runColumnNames.has("heartbeat_at")) {
|
|
144
389
|
sqlite.exec("ALTER TABLE runs ADD COLUMN heartbeat_at TEXT");
|
|
145
390
|
}
|
|
391
|
+
if (!runColumnNames.has("parent_run_id")) {
|
|
392
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN parent_run_id TEXT");
|
|
393
|
+
}
|
|
394
|
+
if (!runColumnNames.has("triggered_by_action_json")) {
|
|
395
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN triggered_by_action_json TEXT");
|
|
396
|
+
}
|
|
397
|
+
if (!runColumnNames.has("source_proposal_id")) {
|
|
398
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN source_proposal_id TEXT");
|
|
399
|
+
}
|
|
400
|
+
if (!runColumnNames.has("source_apply_plan_id")) {
|
|
401
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN source_apply_plan_id TEXT");
|
|
402
|
+
}
|
|
403
|
+
if (!runColumnNames.has("repo_provider")) {
|
|
404
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN repo_provider TEXT");
|
|
405
|
+
}
|
|
406
|
+
if (!runColumnNames.has("repo_owner")) {
|
|
407
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN repo_owner TEXT");
|
|
408
|
+
}
|
|
409
|
+
if (!runColumnNames.has("repo_name")) {
|
|
410
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN repo_name TEXT");
|
|
411
|
+
}
|
|
412
|
+
if (!runColumnNames.has("work_thread_id")) {
|
|
413
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN work_thread_id TEXT");
|
|
414
|
+
}
|
|
415
|
+
if (!runColumnNames.has("conversation_key")) {
|
|
416
|
+
sqlite.exec("ALTER TABLE runs ADD COLUMN conversation_key TEXT");
|
|
417
|
+
}
|
|
418
|
+
sqlite.exec("CREATE INDEX IF NOT EXISTS runs_repo_idx ON runs(repo_provider, repo_owner, repo_name)");
|
|
419
|
+
sqlite.exec("CREATE INDEX IF NOT EXISTS runs_work_thread_idx ON runs(work_thread_id)");
|
|
420
|
+
sqlite.exec("CREATE INDEX IF NOT EXISTS runs_conversation_idx ON runs(conversation_key)");
|
|
421
|
+
sqlite.exec(`
|
|
422
|
+
UPDATE runs
|
|
423
|
+
SET event_id = event_id || '#duplicate:' || id
|
|
424
|
+
WHERE rowid NOT IN (
|
|
425
|
+
SELECT MIN(rowid)
|
|
426
|
+
FROM runs
|
|
427
|
+
GROUP BY event_id
|
|
428
|
+
)
|
|
429
|
+
AND event_id IN (
|
|
430
|
+
SELECT event_id
|
|
431
|
+
FROM runs
|
|
432
|
+
GROUP BY event_id
|
|
433
|
+
HAVING COUNT(*) > 1
|
|
434
|
+
);
|
|
435
|
+
`);
|
|
436
|
+
sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS runs_source_event_id_idx ON runs(event_id)");
|
|
437
|
+
const runEventColumns = sqlite.prepare("PRAGMA table_info(run_events)").all();
|
|
438
|
+
const runEventColumnNames = new Set(runEventColumns.map((column) => column.name));
|
|
439
|
+
if (!runEventColumnNames.has("visibility")) {
|
|
440
|
+
sqlite.exec("ALTER TABLE run_events ADD COLUMN visibility TEXT NOT NULL DEFAULT 'audit'");
|
|
441
|
+
}
|
|
442
|
+
if (!runEventColumnNames.has("importance")) {
|
|
443
|
+
sqlite.exec("ALTER TABLE run_events ADD COLUMN importance TEXT NOT NULL DEFAULT 'normal'");
|
|
444
|
+
}
|
|
445
|
+
if (!runEventColumnNames.has("message")) {
|
|
446
|
+
sqlite.exec("ALTER TABLE run_events ADD COLUMN message TEXT");
|
|
447
|
+
}
|
|
448
|
+
sqlite.exec("CREATE INDEX IF NOT EXISTS run_events_run_idx ON run_events(run_id)");
|
|
449
|
+
sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS repo_policy_rules_repo_id_idx ON repo_policy_rules(provider, owner, repo, id)");
|
|
450
|
+
sqlite.exec("CREATE UNIQUE INDEX IF NOT EXISTS repo_mutation_mappings_repo_id_idx ON repo_mutation_mappings(provider, owner, repo, id)");
|
|
451
|
+
const callbackColumns = sqlite.prepare("PRAGMA table_info(callback_deliveries)").all();
|
|
452
|
+
const callbackColumnNames = new Set(callbackColumns.map((column) => column.name));
|
|
453
|
+
if (!callbackColumnNames.has("next_attempt_at")) {
|
|
454
|
+
sqlite.exec("ALTER TABLE callback_deliveries ADD COLUMN next_attempt_at TEXT");
|
|
455
|
+
}
|
|
456
|
+
if (!callbackColumnNames.has("metadata_json")) {
|
|
457
|
+
sqlite.exec("ALTER TABLE callback_deliveries ADD COLUMN metadata_json TEXT");
|
|
458
|
+
}
|
|
459
|
+
const legacySlackTable = sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'slack_channel_bindings'").get();
|
|
460
|
+
if (legacySlackTable) {
|
|
461
|
+
sqlite.exec(`
|
|
462
|
+
INSERT OR IGNORE INTO channel_bindings (
|
|
463
|
+
provider,
|
|
464
|
+
account_id,
|
|
465
|
+
conversation_id,
|
|
466
|
+
repo_provider,
|
|
467
|
+
owner,
|
|
468
|
+
repo,
|
|
469
|
+
created_at
|
|
470
|
+
)
|
|
471
|
+
SELECT
|
|
472
|
+
'slack',
|
|
473
|
+
team_id,
|
|
474
|
+
channel_id,
|
|
475
|
+
'github',
|
|
476
|
+
owner,
|
|
477
|
+
repo,
|
|
478
|
+
created_at
|
|
479
|
+
FROM slack_channel_bindings;
|
|
480
|
+
`);
|
|
481
|
+
}
|
|
146
482
|
}
|
|
147
483
|
|
|
148
484
|
// src/repository.ts
|
|
@@ -154,11 +490,21 @@ function isIsoExpired(iso, now) {
|
|
|
154
490
|
return new Date(iso).getTime() <= now.getTime();
|
|
155
491
|
}
|
|
156
492
|
function runFromRow(row) {
|
|
493
|
+
const event = OpenTagEventSchema.parse(JSON.parse(row.eventJson));
|
|
157
494
|
const result = row.resultJson ? OpenTagRunResultSchema.parse(JSON.parse(row.resultJson)) : void 0;
|
|
495
|
+
const triggeredByAction = row.triggeredByActionJson ? ActionHintSchema.parse(JSON.parse(row.triggeredByActionJson)) : void 0;
|
|
496
|
+
const protocolFields = protocolRunFieldsFromEvent(event, row.createdAt);
|
|
497
|
+
const contextPacket = row.contextPacketJson ? ContextPacketSchema.parse(JSON.parse(row.contextPacketJson)) : protocolFields.contextPacket;
|
|
158
498
|
return {
|
|
159
499
|
id: row.id,
|
|
160
500
|
eventId: row.eventId,
|
|
161
501
|
status: row.status,
|
|
502
|
+
...protocolFields.thread ? { thread: protocolFields.thread } : {},
|
|
503
|
+
contextPacket,
|
|
504
|
+
...row.parentRunId ? { parentRunId: row.parentRunId } : {},
|
|
505
|
+
...triggeredByAction ? { triggeredByAction } : {},
|
|
506
|
+
...row.sourceProposalId ? { sourceProposalId: row.sourceProposalId } : {},
|
|
507
|
+
...row.sourceApplyPlanId ? { sourceApplyPlanId: row.sourceApplyPlanId } : {},
|
|
162
508
|
assignedRunnerId: row.assignedRunnerId ?? void 0,
|
|
163
509
|
executor: row.executor ?? void 0,
|
|
164
510
|
createdAt: row.createdAt,
|
|
@@ -166,14 +512,222 @@ function runFromRow(row) {
|
|
|
166
512
|
...result ? { result } : {}
|
|
167
513
|
};
|
|
168
514
|
}
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
515
|
+
function callbackDeliveryFromRow(row) {
|
|
516
|
+
const metadata = row.metadataJson && typeof row.metadataJson === "string" ? JSON.parse(row.metadataJson) : void 0;
|
|
517
|
+
return {
|
|
518
|
+
id: row.id,
|
|
519
|
+
runId: row.runId,
|
|
520
|
+
kind: row.kind,
|
|
521
|
+
provider: row.provider,
|
|
522
|
+
uri: row.uri,
|
|
523
|
+
body: row.body,
|
|
524
|
+
...row.threadKey ? { threadKey: row.threadKey } : {},
|
|
525
|
+
...metadata?.agentId ? { agentId: metadata.agentId } : {},
|
|
526
|
+
...metadata?.statusMessageKey ? { statusMessageKey: metadata.statusMessageKey } : {},
|
|
527
|
+
...metadata?.blocks ? { blocks: metadata.blocks } : {},
|
|
528
|
+
status: row.status,
|
|
529
|
+
attempts: row.attempts,
|
|
530
|
+
...row.lastError ? { lastError: row.lastError } : {},
|
|
531
|
+
...row.nextAttemptAt ? { nextAttemptAt: row.nextAttemptAt } : {},
|
|
532
|
+
createdAt: row.createdAt,
|
|
533
|
+
updatedAt: row.updatedAt
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function followUpRequestFromRow(row) {
|
|
537
|
+
return {
|
|
538
|
+
id: row.id,
|
|
539
|
+
sourceEventId: row.sourceEventId,
|
|
540
|
+
conversationKey: row.conversationKey,
|
|
541
|
+
...row.activeRunId ? { activeRunId: row.activeRunId } : {},
|
|
542
|
+
event: OpenTagEventSchema.parse(JSON.parse(row.eventJson)),
|
|
543
|
+
decision: RunAdmissionDecisionSchema.parse(JSON.parse(row.decisionJson)),
|
|
544
|
+
status: row.status,
|
|
545
|
+
...row.createdRunId ? { createdRunId: row.createdRunId } : {},
|
|
546
|
+
createdAt: row.createdAt,
|
|
547
|
+
updatedAt: row.updatedAt
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function runnerFromRow(row) {
|
|
173
551
|
return {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
552
|
+
runnerId: row.runnerId,
|
|
553
|
+
name: row.name,
|
|
554
|
+
createdAt: row.createdAt,
|
|
555
|
+
...row.heartbeatAt ? { heartbeatAt: row.heartbeatAt } : {}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function recordFromJson(value) {
|
|
559
|
+
if (!value) return void 0;
|
|
560
|
+
try {
|
|
561
|
+
const parsed = JSON.parse(value);
|
|
562
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
563
|
+
} catch {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function channelBindingFromRow(row) {
|
|
568
|
+
const metadata = recordFromJson(row.metadataJson);
|
|
569
|
+
return {
|
|
570
|
+
provider: row.provider,
|
|
571
|
+
accountId: row.accountId,
|
|
572
|
+
conversationId: row.conversationId,
|
|
573
|
+
repoProvider: row.repoProvider,
|
|
574
|
+
owner: row.owner,
|
|
575
|
+
repo: row.repo,
|
|
576
|
+
...metadata ? { metadata } : {}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function syntheticManualApprovalPolicyRules(decision) {
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
id: `manual_approval_${decision.id}`,
|
|
583
|
+
scope: "primary_anchor_override",
|
|
584
|
+
effect: "allow",
|
|
585
|
+
reason: "Manual approval decision authorized selected proposal intents."
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
function executorConditionsFromIntent(intent) {
|
|
590
|
+
const value = intent.params?.["executorConditions"];
|
|
591
|
+
if (!Array.isArray(value)) return [];
|
|
592
|
+
return value.filter((condition) => typeof condition === "string" && condition.length > 0);
|
|
593
|
+
}
|
|
594
|
+
function lineageScopeKey(input) {
|
|
595
|
+
return input.snapshot.workThread?.id ?? `run:${input.runId}`;
|
|
596
|
+
}
|
|
597
|
+
function computeProposalLineage(snapshots, targetScopeKey) {
|
|
598
|
+
const scoped = snapshots.filter((snapshot) => lineageScopeKey(snapshot) === targetScopeKey).sort((left, right) => {
|
|
599
|
+
const timeDelta = new Date(left.snapshot.createdAt).getTime() - new Date(right.snapshot.createdAt).getTime();
|
|
600
|
+
if (timeDelta !== 0) return timeDelta;
|
|
601
|
+
return left.snapshot.proposalId.localeCompare(right.snapshot.proposalId);
|
|
602
|
+
});
|
|
603
|
+
const latestProposalByDomain = /* @__PURE__ */ new Map();
|
|
604
|
+
const explicitSupersession = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const stored of scoped) {
|
|
606
|
+
const domainsInProposal = /* @__PURE__ */ new Set();
|
|
607
|
+
for (const intent of stored.snapshot.intents) {
|
|
608
|
+
domainsInProposal.add(intent.domain);
|
|
609
|
+
for (const supersededIntentId of intent.supersedesIntentIds ?? []) {
|
|
610
|
+
explicitSupersession.set(supersededIntentId, { proposalId: stored.snapshot.proposalId, intentId: intent.intentId });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
for (const domain of domainsInProposal) {
|
|
614
|
+
latestProposalByDomain.set(domain, stored.snapshot.proposalId);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const entries = [];
|
|
618
|
+
for (const stored of scoped) {
|
|
619
|
+
for (const intent of stored.snapshot.intents) {
|
|
620
|
+
const explicit = explicitSupersession.get(intent.intentId);
|
|
621
|
+
const latestProposalId = latestProposalByDomain.get(intent.domain);
|
|
622
|
+
if (explicit) {
|
|
623
|
+
entries.push({
|
|
624
|
+
proposalId: stored.snapshot.proposalId,
|
|
625
|
+
intentId: intent.intentId,
|
|
626
|
+
domain: intent.domain,
|
|
627
|
+
status: "superseded",
|
|
628
|
+
supersededByProposalId: explicit.proposalId,
|
|
629
|
+
supersededByIntentId: explicit.intentId,
|
|
630
|
+
reason: "A later intent explicitly superseded this intent."
|
|
631
|
+
});
|
|
632
|
+
} else if (latestProposalId && latestProposalId !== stored.snapshot.proposalId) {
|
|
633
|
+
const supersedingIntent = scoped.find((candidate) => candidate.snapshot.proposalId === latestProposalId)?.snapshot.intents.find((candidateIntent) => candidateIntent.domain === intent.domain);
|
|
634
|
+
entries.push({
|
|
635
|
+
proposalId: stored.snapshot.proposalId,
|
|
636
|
+
intentId: intent.intentId,
|
|
637
|
+
domain: intent.domain,
|
|
638
|
+
status: "superseded",
|
|
639
|
+
supersededByProposalId: latestProposalId,
|
|
640
|
+
...supersedingIntent ? { supersededByIntentId: supersedingIntent.intentId } : {},
|
|
641
|
+
reason: `A newer proposal superseded the ${intent.domain} domain.`
|
|
642
|
+
});
|
|
643
|
+
} else {
|
|
644
|
+
entries.push({
|
|
645
|
+
proposalId: stored.snapshot.proposalId,
|
|
646
|
+
intentId: intent.intentId,
|
|
647
|
+
domain: intent.domain,
|
|
648
|
+
status: "current"
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return ProposalLineageSchema.parse({ scopeKey: targetScopeKey, entries });
|
|
654
|
+
}
|
|
655
|
+
function emptyApplyOutcomeCounts() {
|
|
656
|
+
return {
|
|
657
|
+
applied: 0,
|
|
658
|
+
skipped: 0,
|
|
659
|
+
failed: 0,
|
|
660
|
+
stale: 0,
|
|
661
|
+
unsupported: 0
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function recordFromUnknown(value) {
|
|
665
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
666
|
+
}
|
|
667
|
+
function metricsFromEvents(runId, events) {
|
|
668
|
+
const latestApplyPlans = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const event of events) {
|
|
670
|
+
if (event.type !== "apply_plan.created" && event.type !== "apply_plan.executed") continue;
|
|
671
|
+
const parsed = ApplyPlanSchema.safeParse(event.payload);
|
|
672
|
+
if (parsed.success) {
|
|
673
|
+
latestApplyPlans.set(parsed.data.id, parsed.data);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const applyOutcomeCounts = emptyApplyOutcomeCounts();
|
|
677
|
+
for (const plan of latestApplyPlans.values()) {
|
|
678
|
+
for (const outcome of plan.outcomes ?? []) {
|
|
679
|
+
applyOutcomeCounts[outcome.outcome] += 1;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const humanCallbackCount = events.filter((event) => event.visibility === "human" && event.type.startsWith("callback.")).length;
|
|
683
|
+
const auditEventCount = events.filter((event) => event.visibility === "audit").length;
|
|
684
|
+
return {
|
|
685
|
+
runId,
|
|
686
|
+
totalEventCount: events.length,
|
|
687
|
+
humanEventCount: events.filter((event) => event.visibility === "human").length,
|
|
688
|
+
auditEventCount,
|
|
689
|
+
debugEventCount: events.filter((event) => event.visibility === "debug").length,
|
|
690
|
+
humanCallbackCount,
|
|
691
|
+
threadNoiseRatio: auditEventCount === 0 ? humanCallbackCount : humanCallbackCount / auditEventCount,
|
|
692
|
+
suggestedChangesCount: events.filter((event) => event.type === "proposal.snapshot.created").reduce((count, event) => {
|
|
693
|
+
const payload = recordFromUnknown(event.payload);
|
|
694
|
+
const intents = payload?.["intents"];
|
|
695
|
+
return count + (Array.isArray(intents) ? intents.length : 1);
|
|
696
|
+
}, 0),
|
|
697
|
+
approvalDecisionCount: events.filter((event) => event.type === "approval.decision.recorded").length,
|
|
698
|
+
applyPlanCount: latestApplyPlans.size,
|
|
699
|
+
childRunCount: events.filter((event) => event.type === "run.child_created").length,
|
|
700
|
+
applyOutcomeCounts,
|
|
701
|
+
staleIntentCount: applyOutcomeCounts.stale
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function aggregateMetrics(input) {
|
|
705
|
+
const applyOutcomeCounts = emptyApplyOutcomeCounts();
|
|
706
|
+
for (const run of input.runs) {
|
|
707
|
+
applyOutcomeCounts.applied += run.applyOutcomeCounts.applied;
|
|
708
|
+
applyOutcomeCounts.skipped += run.applyOutcomeCounts.skipped;
|
|
709
|
+
applyOutcomeCounts.failed += run.applyOutcomeCounts.failed;
|
|
710
|
+
applyOutcomeCounts.stale += run.applyOutcomeCounts.stale;
|
|
711
|
+
applyOutcomeCounts.unsupported += run.applyOutcomeCounts.unsupported;
|
|
712
|
+
}
|
|
713
|
+
const auditEventCount = input.runs.reduce((sum, run) => sum + run.auditEventCount, 0);
|
|
714
|
+
const humanCallbackCount = input.runs.reduce((sum, run) => sum + run.humanCallbackCount, 0);
|
|
715
|
+
return {
|
|
716
|
+
scope: input.scope,
|
|
717
|
+
scopeId: input.scopeId,
|
|
718
|
+
runCount: input.runs.length,
|
|
719
|
+
totalEventCount: input.runs.reduce((sum, run) => sum + run.totalEventCount, 0),
|
|
720
|
+
humanEventCount: input.runs.reduce((sum, run) => sum + run.humanEventCount, 0),
|
|
721
|
+
auditEventCount,
|
|
722
|
+
debugEventCount: input.runs.reduce((sum, run) => sum + run.debugEventCount, 0),
|
|
723
|
+
humanCallbackCount,
|
|
724
|
+
threadNoiseRatio: auditEventCount === 0 ? humanCallbackCount : humanCallbackCount / auditEventCount,
|
|
725
|
+
suggestedChangesCount: input.runs.reduce((sum, run) => sum + run.suggestedChangesCount, 0),
|
|
726
|
+
approvalDecisionCount: input.runs.reduce((sum, run) => sum + run.approvalDecisionCount, 0),
|
|
727
|
+
applyPlanCount: input.runs.reduce((sum, run) => sum + run.applyPlanCount, 0),
|
|
728
|
+
childRunCount: input.runs.reduce((sum, run) => sum + run.childRunCount, 0),
|
|
729
|
+
applyOutcomeCounts,
|
|
730
|
+
staleIntentCount: input.runs.reduce((sum, run) => sum + run.staleIntentCount, 0)
|
|
177
731
|
};
|
|
178
732
|
}
|
|
179
733
|
function createOpenTagRepository(db) {
|
|
@@ -181,16 +735,227 @@ function createOpenTagRepository(db) {
|
|
|
181
735
|
await db.insert(runEvents).values({
|
|
182
736
|
runId: input.runId,
|
|
183
737
|
type: input.type,
|
|
738
|
+
visibility: input.visibility ?? defaultRunEventMetadata(input.type).visibility,
|
|
739
|
+
importance: input.importance ?? defaultRunEventMetadata(input.type).importance,
|
|
740
|
+
message: input.message ?? null,
|
|
184
741
|
payloadJson: JSON.stringify(input.payload),
|
|
185
742
|
createdAt: input.createdAt ?? nowIso()
|
|
186
743
|
});
|
|
187
744
|
}
|
|
745
|
+
async function buildApplyPlan(input) {
|
|
746
|
+
const storedProposalRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
|
|
747
|
+
const decisionRow = await db.select().from(approvalDecisions).where(eq(approvalDecisions.id, input.approvalDecisionId)).limit(1).get();
|
|
748
|
+
const decision = decisionRow ? ApprovalDecisionSchema.parse(JSON.parse(decisionRow.decisionJson)) : null;
|
|
749
|
+
if (!storedProposalRow || !decision || decision.proposalId !== input.proposalId) return null;
|
|
750
|
+
const storedProposal = {
|
|
751
|
+
runId: storedProposalRow.runId,
|
|
752
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(storedProposalRow.snapshotJson))
|
|
753
|
+
};
|
|
754
|
+
const runRow = await db.select().from(runs).where(eq(runs.id, storedProposal.runId)).limit(1).get();
|
|
755
|
+
if (!runRow) return null;
|
|
756
|
+
const event = OpenTagEventSchema.parse(JSON.parse(runRow.eventJson));
|
|
757
|
+
const repoKey = projectTargetRefFromEvent(event);
|
|
758
|
+
const storedPolicyRuleRows = repoKey ? await db.select().from(repoPolicyRules).where(and(eq(repoPolicyRules.provider, repoKey.provider), eq(repoPolicyRules.owner, repoKey.owner), eq(repoPolicyRules.repo, repoKey.repo))).orderBy(asc(repoPolicyRules.createdAt)) : [];
|
|
759
|
+
const storedPolicyRules = storedPolicyRuleRows.map((row) => PolicyRuleSchema.parse(JSON.parse(row.ruleJson)));
|
|
760
|
+
const storedMappingRows = repoKey ? await db.select().from(repoMutationMappings).where(
|
|
761
|
+
and(
|
|
762
|
+
eq(repoMutationMappings.provider, repoKey.provider),
|
|
763
|
+
eq(repoMutationMappings.owner, repoKey.owner),
|
|
764
|
+
eq(repoMutationMappings.repo, repoKey.repo)
|
|
765
|
+
)
|
|
766
|
+
).orderBy(asc(repoMutationMappings.createdAt)) : [];
|
|
767
|
+
const storedMappings = storedMappingRows.map((row) => AdapterMutationMappingSchema.parse(JSON.parse(row.mappingJson)));
|
|
768
|
+
const selectedIntentIds = input.selectedIntentIds ?? decision.approvedIntentIds;
|
|
769
|
+
const approvedIntentIds = new Set(decision.approvedIntentIds);
|
|
770
|
+
const proposalIntents = new Map(storedProposal.snapshot.intents.map((intent) => [intent.intentId, intent]));
|
|
771
|
+
const lineageRows = await db.select().from(suggestedChanges).orderBy(asc(suggestedChanges.createdAt));
|
|
772
|
+
const lineage = computeProposalLineage(
|
|
773
|
+
lineageRows.map((row) => ({
|
|
774
|
+
runId: row.runId,
|
|
775
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson))
|
|
776
|
+
})),
|
|
777
|
+
lineageScopeKey(storedProposal)
|
|
778
|
+
);
|
|
779
|
+
const actionabilityByIntentId = new Map(lineage.entries.map((entry) => [entry.intentId, entry]));
|
|
780
|
+
const policyRules = [...storedPolicyRules, ...input.policyRules ?? [], ...syntheticManualApprovalPolicyRules(decision)];
|
|
781
|
+
const outcomes = selectedIntentIds.map((intentId) => {
|
|
782
|
+
if (!approvedIntentIds.has(intentId)) {
|
|
783
|
+
return {
|
|
784
|
+
intentId,
|
|
785
|
+
outcome: "skipped",
|
|
786
|
+
message: "Intent was not approved by the approval decision."
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const intent = proposalIntents.get(intentId);
|
|
790
|
+
if (!intent) {
|
|
791
|
+
return {
|
|
792
|
+
intentId,
|
|
793
|
+
outcome: "failed",
|
|
794
|
+
message: "Intent does not exist on the referenced proposal."
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const actionability = actionabilityByIntentId.get(intentId);
|
|
798
|
+
if (actionability?.status !== "current") {
|
|
799
|
+
return {
|
|
800
|
+
intentId,
|
|
801
|
+
outcome: "stale",
|
|
802
|
+
message: actionability?.reason ?? "Intent is no longer current for its mutation domain."
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
return preflightMutationIntent({
|
|
806
|
+
intent,
|
|
807
|
+
permissions: event.permissions,
|
|
808
|
+
policyRules,
|
|
809
|
+
executorConditions: executorConditionsFromIntent(intent),
|
|
810
|
+
...input.adapter ? { adapter: input.adapter } : {}
|
|
811
|
+
}).outcome;
|
|
812
|
+
});
|
|
813
|
+
return {
|
|
814
|
+
runId: storedProposal.runId,
|
|
815
|
+
createdAt: nowIso(),
|
|
816
|
+
plan: ApplyPlanSchema.parse({
|
|
817
|
+
id: input.id,
|
|
818
|
+
proposalId: input.proposalId,
|
|
819
|
+
approvalDecisionId: input.approvalDecisionId,
|
|
820
|
+
selectedIntentIds,
|
|
821
|
+
...input.adapter ? { adapter: input.adapter } : {},
|
|
822
|
+
adapterPlan: {
|
|
823
|
+
semantics: "preflight first, then per-intent outcome",
|
|
824
|
+
externalWritesExecuted: false,
|
|
825
|
+
mappings: storedMappings
|
|
826
|
+
},
|
|
827
|
+
outcomes
|
|
828
|
+
})
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function applyPlanCreatedEventRow(input) {
|
|
832
|
+
return {
|
|
833
|
+
runId: input.runId,
|
|
834
|
+
type: "apply_plan.created",
|
|
835
|
+
visibility: "audit",
|
|
836
|
+
importance: "high",
|
|
837
|
+
message: `Created apply plan for ${input.plan.selectedIntentIds.length} intent(s).`,
|
|
838
|
+
payloadJson: JSON.stringify(input.plan),
|
|
839
|
+
createdAt: input.createdAt
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function appendApplyPlanCreatedEvent(input) {
|
|
843
|
+
await db.insert(runEvents).values(applyPlanCreatedEventRow(input));
|
|
844
|
+
}
|
|
188
845
|
return {
|
|
189
846
|
appendRunEvent,
|
|
847
|
+
async getRunByEventId(input) {
|
|
848
|
+
const row = await db.select().from(runs).where(eq(runs.eventId, input.eventId)).limit(1).get();
|
|
849
|
+
if (!row) return null;
|
|
850
|
+
return {
|
|
851
|
+
run: runFromRow(row),
|
|
852
|
+
event: OpenTagEventSchema.parse(JSON.parse(row.eventJson))
|
|
853
|
+
};
|
|
854
|
+
},
|
|
855
|
+
async findActiveRunForConversation(input) {
|
|
856
|
+
const row = await db.select().from(runs).where(and(eq(runs.conversationKey, input.conversationKey), inArray(runs.status, ["assigned", "running"]))).orderBy(asc(runs.createdAt)).limit(1).get();
|
|
857
|
+
if (!row) return null;
|
|
858
|
+
return {
|
|
859
|
+
run: runFromRow(row),
|
|
860
|
+
event: OpenTagEventSchema.parse(JSON.parse(row.eventJson))
|
|
861
|
+
};
|
|
862
|
+
},
|
|
863
|
+
async createFollowUpRequest(input) {
|
|
864
|
+
const event = OpenTagEventSchema.parse(input.event);
|
|
865
|
+
const decision = RunAdmissionDecisionSchema.parse(input.decision);
|
|
866
|
+
const createdAt = nowIso();
|
|
867
|
+
const conversationKey = conversationKeyFromEvent(event);
|
|
868
|
+
const insertResult = await db.insert(followUpRequests).values({
|
|
869
|
+
id: input.id,
|
|
870
|
+
sourceEventId: event.id,
|
|
871
|
+
conversationKey,
|
|
872
|
+
activeRunId: input.activeRunId ?? null,
|
|
873
|
+
eventJson: JSON.stringify(event),
|
|
874
|
+
decisionJson: JSON.stringify(decision),
|
|
875
|
+
status: "queued",
|
|
876
|
+
createdRunId: null,
|
|
877
|
+
createdAt,
|
|
878
|
+
updatedAt: createdAt
|
|
879
|
+
}).onConflictDoNothing({ target: followUpRequests.sourceEventId });
|
|
880
|
+
if (insertResult.changes === 0) {
|
|
881
|
+
const existing = await db.select().from(followUpRequests).where(eq(followUpRequests.sourceEventId, event.id)).limit(1).get();
|
|
882
|
+
if (!existing) {
|
|
883
|
+
throw new Error(`Follow-up request already exists for event ${event.id}, but it could not be loaded`);
|
|
884
|
+
}
|
|
885
|
+
return { followUpRequest: followUpRequestFromRow(existing), created: false };
|
|
886
|
+
}
|
|
887
|
+
const created = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.id)).limit(1).get();
|
|
888
|
+
if (!created) {
|
|
889
|
+
throw new Error(`Follow-up request ${input.id} was created but could not be loaded`);
|
|
890
|
+
}
|
|
891
|
+
return { followUpRequest: followUpRequestFromRow(created), created: true };
|
|
892
|
+
},
|
|
893
|
+
async getFollowUpRequest(input) {
|
|
894
|
+
const row = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.id)).limit(1).get();
|
|
895
|
+
return row ? followUpRequestFromRow(row) : null;
|
|
896
|
+
},
|
|
897
|
+
async createRunFromFollowUpRequest(input) {
|
|
898
|
+
const row = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.followUpRequestId)).limit(1).get();
|
|
899
|
+
if (!row) {
|
|
900
|
+
throw new Error(`Follow-up request not found: ${input.followUpRequestId}`);
|
|
901
|
+
}
|
|
902
|
+
if (row.status !== "queued") {
|
|
903
|
+
throw new Error(`Follow-up request ${input.followUpRequestId} is not queued.`);
|
|
904
|
+
}
|
|
905
|
+
const updatedAt = nowIso();
|
|
906
|
+
const promoteResult = await db.update(followUpRequests).set({
|
|
907
|
+
status: "promoting",
|
|
908
|
+
updatedAt
|
|
909
|
+
}).where(and(eq(followUpRequests.id, input.followUpRequestId), eq(followUpRequests.status, "queued")));
|
|
910
|
+
if (promoteResult.changes === 0) {
|
|
911
|
+
throw new Error(`Follow-up request ${input.followUpRequestId} is not queued.`);
|
|
912
|
+
}
|
|
913
|
+
const followUp = followUpRequestFromRow({ ...row, status: "promoting", updatedAt });
|
|
914
|
+
try {
|
|
915
|
+
const { run, created } = await this.createRun({
|
|
916
|
+
id: input.runId,
|
|
917
|
+
event: followUp.event,
|
|
918
|
+
...followUp.activeRunId ? { parentRunId: followUp.activeRunId } : {}
|
|
919
|
+
});
|
|
920
|
+
if (!created) {
|
|
921
|
+
throw new Error(`Run already exists for follow-up request ${input.followUpRequestId}.`);
|
|
922
|
+
}
|
|
923
|
+
await db.update(followUpRequests).set({
|
|
924
|
+
status: "promoted",
|
|
925
|
+
createdRunId: run.id,
|
|
926
|
+
updatedAt
|
|
927
|
+
}).where(eq(followUpRequests.id, input.followUpRequestId));
|
|
928
|
+
const updated = await db.select().from(followUpRequests).where(eq(followUpRequests.id, input.followUpRequestId)).limit(1).get();
|
|
929
|
+
if (!updated) {
|
|
930
|
+
throw new Error(`Follow-up request ${input.followUpRequestId} was promoted but could not be loaded`);
|
|
931
|
+
}
|
|
932
|
+
if (followUp.activeRunId) {
|
|
933
|
+
await appendRunEvent({
|
|
934
|
+
runId: followUp.activeRunId,
|
|
935
|
+
type: "follow_up_request.promoted",
|
|
936
|
+
payload: { followUpRequestId: followUp.id, createdRunId: run.id, sourceEventId: followUp.sourceEventId },
|
|
937
|
+
visibility: "audit",
|
|
938
|
+
importance: "normal",
|
|
939
|
+
createdAt: updatedAt
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return { followUpRequest: followUpRequestFromRow(updated), run };
|
|
943
|
+
} catch (error) {
|
|
944
|
+
await db.update(followUpRequests).set({
|
|
945
|
+
status: "queued",
|
|
946
|
+
updatedAt: nowIso()
|
|
947
|
+
}).where(and(eq(followUpRequests.id, input.followUpRequestId), eq(followUpRequests.status, "promoting")));
|
|
948
|
+
throw error;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
190
951
|
async registerRunner(input) {
|
|
191
952
|
const createdAt = nowIso();
|
|
192
953
|
await db.insert(runners).values({ runnerId: input.runnerId, name: input.name, createdAt }).onConflictDoNothing();
|
|
193
954
|
},
|
|
955
|
+
async getRunner(input) {
|
|
956
|
+
const row = await db.select().from(runners).where(eq(runners.runnerId, input.runnerId)).limit(1).get();
|
|
957
|
+
return row ? runnerFromRow(row) : null;
|
|
958
|
+
},
|
|
194
959
|
async createRepoBinding(input) {
|
|
195
960
|
await db.insert(repoBindings).values({
|
|
196
961
|
...input,
|
|
@@ -208,16 +973,87 @@ function createOpenTagRepository(db) {
|
|
|
208
973
|
}
|
|
209
974
|
});
|
|
210
975
|
},
|
|
976
|
+
async upsertRepoPolicyRule(input) {
|
|
977
|
+
const rule = PolicyRuleSchema.parse(input.rule);
|
|
978
|
+
const createdAt = nowIso();
|
|
979
|
+
await db.insert(repoPolicyRules).values({
|
|
980
|
+
id: rule.id,
|
|
981
|
+
provider: input.provider,
|
|
982
|
+
owner: input.owner,
|
|
983
|
+
repo: input.repo,
|
|
984
|
+
ruleJson: JSON.stringify(rule),
|
|
985
|
+
createdAt
|
|
986
|
+
}).onConflictDoUpdate({
|
|
987
|
+
target: [repoPolicyRules.provider, repoPolicyRules.owner, repoPolicyRules.repo, repoPolicyRules.id],
|
|
988
|
+
set: {
|
|
989
|
+
ruleJson: JSON.stringify(rule),
|
|
990
|
+
createdAt
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return rule;
|
|
994
|
+
},
|
|
995
|
+
async listRepoPolicyRules(input) {
|
|
996
|
+
const rows = await db.select().from(repoPolicyRules).where(and(eq(repoPolicyRules.provider, input.provider), eq(repoPolicyRules.owner, input.owner), eq(repoPolicyRules.repo, input.repo))).orderBy(asc(repoPolicyRules.createdAt));
|
|
997
|
+
return rows.map((row) => PolicyRuleSchema.parse(JSON.parse(row.ruleJson)));
|
|
998
|
+
},
|
|
999
|
+
async upsertRepoMutationMapping(input) {
|
|
1000
|
+
const mapping = AdapterMutationMappingSchema.parse(input.mapping);
|
|
1001
|
+
const createdAt = nowIso();
|
|
1002
|
+
await db.insert(repoMutationMappings).values({
|
|
1003
|
+
id: mapping.id,
|
|
1004
|
+
provider: input.provider,
|
|
1005
|
+
owner: input.owner,
|
|
1006
|
+
repo: input.repo,
|
|
1007
|
+
mappingJson: JSON.stringify(mapping),
|
|
1008
|
+
createdAt
|
|
1009
|
+
}).onConflictDoUpdate({
|
|
1010
|
+
target: [repoMutationMappings.provider, repoMutationMappings.owner, repoMutationMappings.repo, repoMutationMappings.id],
|
|
1011
|
+
set: {
|
|
1012
|
+
mappingJson: JSON.stringify(mapping),
|
|
1013
|
+
createdAt
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
return mapping;
|
|
1017
|
+
},
|
|
1018
|
+
async listRepoMutationMappings(input) {
|
|
1019
|
+
const rows = await db.select().from(repoMutationMappings).where(and(eq(repoMutationMappings.provider, input.provider), eq(repoMutationMappings.owner, input.owner), eq(repoMutationMappings.repo, input.repo))).orderBy(asc(repoMutationMappings.createdAt));
|
|
1020
|
+
return rows.map((row) => AdapterMutationMappingSchema.parse(JSON.parse(row.mappingJson)));
|
|
1021
|
+
},
|
|
1022
|
+
async upsertChannelBinding(input) {
|
|
1023
|
+
await db.insert(channelBindings).values({
|
|
1024
|
+
provider: input.provider,
|
|
1025
|
+
accountId: input.accountId,
|
|
1026
|
+
conversationId: input.conversationId,
|
|
1027
|
+
repoProvider: input.repoProvider,
|
|
1028
|
+
owner: input.owner,
|
|
1029
|
+
repo: input.repo,
|
|
1030
|
+
metadataJson: input.metadata ? JSON.stringify(input.metadata) : null,
|
|
1031
|
+
createdAt: nowIso()
|
|
1032
|
+
}).onConflictDoUpdate({
|
|
1033
|
+
target: [channelBindings.provider, channelBindings.accountId, channelBindings.conversationId],
|
|
1034
|
+
set: {
|
|
1035
|
+
repoProvider: input.repoProvider,
|
|
1036
|
+
owner: input.owner,
|
|
1037
|
+
repo: input.repo,
|
|
1038
|
+
metadataJson: input.metadata ? JSON.stringify(input.metadata) : null
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
},
|
|
211
1042
|
async createSlackChannelBinding(input) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1043
|
+
const repoProvider = input.repoProvider ?? "github";
|
|
1044
|
+
await db.insert(channelBindings).values({
|
|
1045
|
+
provider: "slack",
|
|
1046
|
+
accountId: input.teamId,
|
|
1047
|
+
conversationId: input.channelId,
|
|
1048
|
+
repoProvider,
|
|
215
1049
|
owner: input.owner,
|
|
216
1050
|
repo: input.repo,
|
|
1051
|
+
metadataJson: null,
|
|
217
1052
|
createdAt: nowIso()
|
|
218
1053
|
}).onConflictDoUpdate({
|
|
219
|
-
target: [
|
|
1054
|
+
target: [channelBindings.provider, channelBindings.accountId, channelBindings.conversationId],
|
|
220
1055
|
set: {
|
|
1056
|
+
repoProvider,
|
|
221
1057
|
owner: input.owner,
|
|
222
1058
|
repo: input.repo
|
|
223
1059
|
}
|
|
@@ -225,32 +1061,132 @@ function createOpenTagRepository(db) {
|
|
|
225
1061
|
},
|
|
226
1062
|
async createRun(input) {
|
|
227
1063
|
const event = OpenTagEventSchema.parse(input.event);
|
|
1064
|
+
const triggeredByAction = input.triggeredByAction ? ActionHintSchema.parse(input.triggeredByAction) : void 0;
|
|
228
1065
|
const createdAt = nowIso();
|
|
229
|
-
|
|
1066
|
+
const protocolFields = protocolRunFieldsFromEvent(event, createdAt);
|
|
1067
|
+
const repoKey = projectTargetRefFromEvent(event);
|
|
1068
|
+
const insertResult = await db.insert(runs).values({
|
|
230
1069
|
id: input.id,
|
|
231
1070
|
eventId: event.id,
|
|
232
1071
|
status: "queued",
|
|
233
1072
|
eventJson: JSON.stringify(event),
|
|
1073
|
+
contextPacketJson: JSON.stringify(protocolFields.contextPacket),
|
|
1074
|
+
parentRunId: input.parentRunId ?? null,
|
|
1075
|
+
triggeredByActionJson: triggeredByAction ? JSON.stringify(triggeredByAction) : null,
|
|
1076
|
+
sourceProposalId: input.sourceProposalId ?? null,
|
|
1077
|
+
sourceApplyPlanId: input.sourceApplyPlanId ?? null,
|
|
1078
|
+
repoProvider: repoKey?.provider ?? null,
|
|
1079
|
+
repoOwner: repoKey?.owner ?? null,
|
|
1080
|
+
repoName: repoKey?.repo ?? null,
|
|
1081
|
+
workThreadId: protocolFields.thread?.id ?? null,
|
|
1082
|
+
conversationKey: conversationKeyFromEvent(event),
|
|
234
1083
|
createdAt,
|
|
235
1084
|
updatedAt: createdAt
|
|
1085
|
+
}).onConflictDoNothing({ target: runs.eventId });
|
|
1086
|
+
if (insertResult.changes === 0) {
|
|
1087
|
+
const existingBySourceEvent = await db.select().from(runs).where(eq(runs.eventId, event.id)).limit(1).get();
|
|
1088
|
+
if (!existingBySourceEvent) {
|
|
1089
|
+
throw new Error(`Run already exists for event ${event.id}, but it could not be loaded`);
|
|
1090
|
+
}
|
|
1091
|
+
const replayDecision = RunAdmissionDecisionSchema.parse({
|
|
1092
|
+
action: "drop_duplicate",
|
|
1093
|
+
reason: "Source event already created a run.",
|
|
1094
|
+
reasonCode: "duplicate_source_event",
|
|
1095
|
+
decidedAt: createdAt,
|
|
1096
|
+
activeRunId: existingBySourceEvent.id,
|
|
1097
|
+
eventId: event.id
|
|
1098
|
+
});
|
|
1099
|
+
await appendRunEvent({
|
|
1100
|
+
runId: existingBySourceEvent.id,
|
|
1101
|
+
type: "admission.decided",
|
|
1102
|
+
payload: replayDecision,
|
|
1103
|
+
visibility: "audit",
|
|
1104
|
+
importance: "normal",
|
|
1105
|
+
message: replayDecision.reason,
|
|
1106
|
+
createdAt
|
|
1107
|
+
});
|
|
1108
|
+
await appendRunEvent({
|
|
1109
|
+
runId: existingBySourceEvent.id,
|
|
1110
|
+
type: "run.create_idempotent_replay",
|
|
1111
|
+
payload: { requestedRunId: input.id, eventId: event.id },
|
|
1112
|
+
visibility: "audit",
|
|
1113
|
+
importance: "low",
|
|
1114
|
+
createdAt
|
|
1115
|
+
});
|
|
1116
|
+
return { run: runFromRow(existingBySourceEvent), created: false };
|
|
1117
|
+
}
|
|
1118
|
+
const createDecision = RunAdmissionDecisionSchema.parse({
|
|
1119
|
+
action: "start",
|
|
1120
|
+
reason: "Source event accepted and ready to create a run.",
|
|
1121
|
+
reasonCode: "new_event",
|
|
1122
|
+
decidedAt: createdAt,
|
|
1123
|
+
eventId: event.id
|
|
1124
|
+
});
|
|
1125
|
+
await appendRunEvent({
|
|
1126
|
+
runId: input.id,
|
|
1127
|
+
type: "admission.decided",
|
|
1128
|
+
payload: createDecision,
|
|
1129
|
+
visibility: "audit",
|
|
1130
|
+
importance: "normal",
|
|
1131
|
+
message: createDecision.reason,
|
|
1132
|
+
createdAt
|
|
236
1133
|
});
|
|
237
1134
|
await appendRunEvent({
|
|
238
1135
|
runId: input.id,
|
|
239
1136
|
type: "run.created",
|
|
240
1137
|
payload: { eventId: event.id },
|
|
1138
|
+
visibility: "audit",
|
|
1139
|
+
importance: "low",
|
|
1140
|
+
createdAt
|
|
1141
|
+
});
|
|
1142
|
+
await appendRunEvent({
|
|
1143
|
+
runId: input.id,
|
|
1144
|
+
type: "context_packet.generated",
|
|
1145
|
+
payload: {
|
|
1146
|
+
contextPacket: protocolFields.contextPacket,
|
|
1147
|
+
...protocolFields.thread ? { thread: protocolFields.thread } : {}
|
|
1148
|
+
},
|
|
1149
|
+
visibility: "audit",
|
|
1150
|
+
importance: "normal",
|
|
1151
|
+
message: protocolFields.contextPacket.summary,
|
|
241
1152
|
createdAt
|
|
242
1153
|
});
|
|
1154
|
+
if (input.parentRunId) {
|
|
1155
|
+
await appendRunEvent({
|
|
1156
|
+
runId: input.parentRunId,
|
|
1157
|
+
type: "run.child_created",
|
|
1158
|
+
payload: {
|
|
1159
|
+
childRunId: input.id,
|
|
1160
|
+
...triggeredByAction ? { triggeredByAction } : {},
|
|
1161
|
+
...input.sourceProposalId ? { sourceProposalId: input.sourceProposalId } : {},
|
|
1162
|
+
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {}
|
|
1163
|
+
},
|
|
1164
|
+
visibility: "audit",
|
|
1165
|
+
importance: "normal",
|
|
1166
|
+
message: `Created child run ${input.id}.`,
|
|
1167
|
+
createdAt
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
243
1170
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
1171
|
+
run: {
|
|
1172
|
+
id: input.id,
|
|
1173
|
+
eventId: event.id,
|
|
1174
|
+
status: "queued",
|
|
1175
|
+
...protocolFields,
|
|
1176
|
+
...input.parentRunId ? { parentRunId: input.parentRunId } : {},
|
|
1177
|
+
...triggeredByAction ? { triggeredByAction } : {},
|
|
1178
|
+
...input.sourceProposalId ? { sourceProposalId: input.sourceProposalId } : {},
|
|
1179
|
+
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {},
|
|
1180
|
+
contextPacket: protocolFields.contextPacket,
|
|
1181
|
+
createdAt,
|
|
1182
|
+
updatedAt: createdAt
|
|
1183
|
+
},
|
|
1184
|
+
created: true
|
|
249
1185
|
};
|
|
250
1186
|
},
|
|
251
1187
|
async claimNextRun(input) {
|
|
252
1188
|
const now = /* @__PURE__ */ new Date();
|
|
253
|
-
const activeRows = await db.select().from(runs).where(
|
|
1189
|
+
const activeRows = await db.select().from(runs).where(inArray(runs.status, ["assigned", "running"])).orderBy(asc(runs.createdAt));
|
|
254
1190
|
for (const activeRow of activeRows) {
|
|
255
1191
|
if (!isIsoExpired(activeRow.leaseExpiresAt, now)) continue;
|
|
256
1192
|
const updatedAt2 = nowIso();
|
|
@@ -266,13 +1202,15 @@ function createOpenTagRepository(db) {
|
|
|
266
1202
|
runId: activeRow.id,
|
|
267
1203
|
type: "run.lease_expired",
|
|
268
1204
|
payload: { previousRunnerId: activeRow.assignedRunnerId, previousLeaseExpiresAt: activeRow.leaseExpiresAt },
|
|
1205
|
+
visibility: "audit",
|
|
1206
|
+
importance: "normal",
|
|
269
1207
|
createdAt: updatedAt2
|
|
270
1208
|
});
|
|
271
1209
|
}
|
|
272
1210
|
const queuedRows = await db.select().from(runs).where(eq(runs.status, "queued")).orderBy(asc(runs.createdAt));
|
|
273
1211
|
const row = queuedRows.find((candidate) => {
|
|
274
1212
|
const event = OpenTagEventSchema.parse(JSON.parse(candidate.eventJson));
|
|
275
|
-
const repoKey =
|
|
1213
|
+
const repoKey = projectTargetRefFromEvent(event);
|
|
276
1214
|
if (!repoKey) return false;
|
|
277
1215
|
const binding = db.select().from(repoBindings).where(
|
|
278
1216
|
and(
|
|
@@ -288,28 +1226,35 @@ function createOpenTagRepository(db) {
|
|
|
288
1226
|
const updatedAt = nowIso();
|
|
289
1227
|
const leasedAt = updatedAt;
|
|
290
1228
|
const leaseExpiresAt = new Date(Date.now() + input.leaseSeconds * 1e3).toISOString();
|
|
291
|
-
await db.update(runs).set({
|
|
1229
|
+
const updateResult = await db.update(runs).set({
|
|
292
1230
|
status: "assigned",
|
|
293
1231
|
assignedRunnerId: input.runnerId,
|
|
294
1232
|
leasedAt,
|
|
295
1233
|
leaseExpiresAt,
|
|
296
1234
|
heartbeatAt: leasedAt,
|
|
297
1235
|
updatedAt
|
|
298
|
-
}).where(eq(runs.id, row.id));
|
|
1236
|
+
}).where(and(eq(runs.id, row.id), eq(runs.status, "queued")));
|
|
1237
|
+
if (updateResult.changes === 0) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
299
1240
|
await appendRunEvent({
|
|
300
1241
|
runId: row.id,
|
|
301
1242
|
type: "run.claimed",
|
|
302
1243
|
payload: { runnerId: input.runnerId, leasedAt, leaseExpiresAt },
|
|
1244
|
+
visibility: "audit",
|
|
1245
|
+
importance: "normal",
|
|
303
1246
|
createdAt: updatedAt
|
|
304
1247
|
});
|
|
305
1248
|
return {
|
|
306
1249
|
run: {
|
|
307
|
-
|
|
308
|
-
|
|
1250
|
+
...runFromRow({
|
|
1251
|
+
...row,
|
|
1252
|
+
status: "assigned",
|
|
1253
|
+
assignedRunnerId: input.runnerId,
|
|
1254
|
+
updatedAt
|
|
1255
|
+
}),
|
|
309
1256
|
status: "assigned",
|
|
310
1257
|
assignedRunnerId: input.runnerId,
|
|
311
|
-
executor: row.executor ?? void 0,
|
|
312
|
-
createdAt: row.createdAt,
|
|
313
1258
|
updatedAt
|
|
314
1259
|
},
|
|
315
1260
|
event: OpenTagEventSchema.parse(JSON.parse(row.eventJson))
|
|
@@ -330,14 +1275,32 @@ function createOpenTagRepository(db) {
|
|
|
330
1275
|
...row.allowedActorsJson ? { allowedActors: JSON.parse(row.allowedActorsJson) } : {}
|
|
331
1276
|
};
|
|
332
1277
|
},
|
|
1278
|
+
async getChannelBinding(input) {
|
|
1279
|
+
const row = await db.select().from(channelBindings).where(
|
|
1280
|
+
and(
|
|
1281
|
+
eq(channelBindings.provider, input.provider),
|
|
1282
|
+
eq(channelBindings.accountId, input.accountId),
|
|
1283
|
+
eq(channelBindings.conversationId, input.conversationId)
|
|
1284
|
+
)
|
|
1285
|
+
).limit(1).get();
|
|
1286
|
+
return row ? channelBindingFromRow(row) : null;
|
|
1287
|
+
},
|
|
333
1288
|
async getSlackChannelBinding(input) {
|
|
334
|
-
const row = await db.select().from(
|
|
1289
|
+
const row = await db.select().from(channelBindings).where(
|
|
1290
|
+
and(
|
|
1291
|
+
eq(channelBindings.provider, "slack"),
|
|
1292
|
+
eq(channelBindings.accountId, input.teamId),
|
|
1293
|
+
eq(channelBindings.conversationId, input.channelId)
|
|
1294
|
+
)
|
|
1295
|
+
).limit(1).get();
|
|
335
1296
|
if (!row) return null;
|
|
1297
|
+
const binding = channelBindingFromRow(row);
|
|
336
1298
|
return {
|
|
337
|
-
teamId:
|
|
338
|
-
channelId:
|
|
339
|
-
|
|
340
|
-
|
|
1299
|
+
teamId: binding.accountId,
|
|
1300
|
+
channelId: binding.conversationId,
|
|
1301
|
+
repoProvider: binding.repoProvider,
|
|
1302
|
+
owner: binding.owner,
|
|
1303
|
+
repo: binding.repo
|
|
341
1304
|
};
|
|
342
1305
|
},
|
|
343
1306
|
async heartbeat(input) {
|
|
@@ -351,42 +1314,311 @@ function createOpenTagRepository(db) {
|
|
|
351
1314
|
runId: input.runId,
|
|
352
1315
|
type: "run.heartbeat",
|
|
353
1316
|
payload: { runnerId: input.runnerId, heartbeatAt: updatedAt, leaseExpiresAt },
|
|
1317
|
+
visibility: "debug",
|
|
1318
|
+
importance: "low",
|
|
354
1319
|
createdAt: updatedAt
|
|
355
1320
|
});
|
|
356
1321
|
return true;
|
|
357
1322
|
},
|
|
358
1323
|
async markRunning(input) {
|
|
359
1324
|
const updatedAt = nowIso();
|
|
360
|
-
|
|
1325
|
+
const conditions = [eq(runs.id, input.runId)];
|
|
1326
|
+
if (input.runnerId) {
|
|
1327
|
+
conditions.push(eq(runs.assignedRunnerId, input.runnerId));
|
|
1328
|
+
}
|
|
1329
|
+
const updateResult = await db.update(runs).set({ status: "running", executor: input.executor, updatedAt }).where(and(...conditions));
|
|
1330
|
+
if (updateResult.changes === 0) {
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
361
1333
|
await appendRunEvent({
|
|
362
1334
|
runId: input.runId,
|
|
363
1335
|
type: "run.running",
|
|
364
|
-
payload: { executor: input.executor },
|
|
1336
|
+
payload: input.runnerId ? { runnerId: input.runnerId, executor: input.executor } : { executor: input.executor },
|
|
1337
|
+
visibility: "audit",
|
|
1338
|
+
importance: "normal",
|
|
365
1339
|
createdAt: updatedAt
|
|
366
1340
|
});
|
|
1341
|
+
return true;
|
|
367
1342
|
},
|
|
368
1343
|
async completeRun(input) {
|
|
369
1344
|
const result = OpenTagRunResultSchema.parse(input.result);
|
|
370
1345
|
const updatedAt = nowIso();
|
|
371
|
-
const status = result.conclusion === "success" ? "succeeded" : result.conclusion === "cancelled" ? "cancelled" : "failed";
|
|
372
|
-
await db.
|
|
1346
|
+
const status = result.conclusion === "success" ? "succeeded" : result.conclusion === "cancelled" ? "cancelled" : result.conclusion === "needs_human" ? "needs_approval" : "failed";
|
|
1347
|
+
const runRow = await db.select().from(runs).where(eq(runs.id, input.runId)).limit(1).get();
|
|
1348
|
+
if (!runRow) {
|
|
1349
|
+
if (input.runnerId) return false;
|
|
1350
|
+
throw new Error(`Run not found: ${input.runId}`);
|
|
1351
|
+
}
|
|
1352
|
+
if (input.runnerId && runRow.assignedRunnerId !== input.runnerId) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
const runThread = runRow ? protocolRunFieldsFromEvent(OpenTagEventSchema.parse(JSON.parse(runRow.eventJson)), runRow.createdAt).thread : void 0;
|
|
1356
|
+
await db.update(runs).set({
|
|
1357
|
+
status,
|
|
1358
|
+
resultJson: JSON.stringify(result),
|
|
1359
|
+
assignedRunnerId: null,
|
|
1360
|
+
leasedAt: null,
|
|
1361
|
+
leaseExpiresAt: null,
|
|
1362
|
+
heartbeatAt: null,
|
|
1363
|
+
updatedAt
|
|
1364
|
+
}).where(input.runnerId ? and(eq(runs.id, input.runId), eq(runs.assignedRunnerId, input.runnerId)) : eq(runs.id, input.runId));
|
|
1365
|
+
for (const snapshot of result.suggestedChanges ?? []) {
|
|
1366
|
+
const parsedSnapshot = SuggestedChangesSnapshotSchema.parse({
|
|
1367
|
+
...snapshot,
|
|
1368
|
+
sourceRunId: snapshot.sourceRunId ?? input.runId,
|
|
1369
|
+
...snapshot.workThread || !runThread ? {} : { workThread: runThread }
|
|
1370
|
+
});
|
|
1371
|
+
await db.insert(suggestedChanges).values({
|
|
1372
|
+
proposalId: parsedSnapshot.proposalId,
|
|
1373
|
+
runId: input.runId,
|
|
1374
|
+
snapshotJson: JSON.stringify(parsedSnapshot),
|
|
1375
|
+
createdAt: parsedSnapshot.createdAt
|
|
1376
|
+
}).onConflictDoUpdate({
|
|
1377
|
+
target: suggestedChanges.proposalId,
|
|
1378
|
+
set: {
|
|
1379
|
+
runId: input.runId,
|
|
1380
|
+
snapshotJson: JSON.stringify(parsedSnapshot),
|
|
1381
|
+
createdAt: parsedSnapshot.createdAt
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
await appendRunEvent({
|
|
1385
|
+
runId: input.runId,
|
|
1386
|
+
type: "proposal.snapshot.created",
|
|
1387
|
+
payload: parsedSnapshot,
|
|
1388
|
+
visibility: "audit",
|
|
1389
|
+
importance: "high",
|
|
1390
|
+
message: parsedSnapshot.summary,
|
|
1391
|
+
createdAt: updatedAt
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
373
1394
|
await appendRunEvent({
|
|
374
1395
|
runId: input.runId,
|
|
375
1396
|
type: "run.completed",
|
|
376
1397
|
payload: result,
|
|
1398
|
+
visibility: "audit",
|
|
1399
|
+
importance: "high",
|
|
1400
|
+
message: result.summary,
|
|
377
1401
|
createdAt: updatedAt
|
|
378
1402
|
});
|
|
1403
|
+
if ((result.suggestedChanges?.length ?? 0) > 0 || (result.artifacts?.length ?? 0) > 0) {
|
|
1404
|
+
await appendRunEvent({
|
|
1405
|
+
runId: input.runId,
|
|
1406
|
+
type: "success_metric.observed",
|
|
1407
|
+
payload: {
|
|
1408
|
+
metric: "time_to_first_useful_artifact",
|
|
1409
|
+
artifactCount: result.artifacts?.length ?? 0,
|
|
1410
|
+
suggestedChangesCount: result.suggestedChanges?.length ?? 0
|
|
1411
|
+
},
|
|
1412
|
+
visibility: "audit",
|
|
1413
|
+
importance: "normal",
|
|
1414
|
+
createdAt: updatedAt
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
return true;
|
|
1418
|
+
},
|
|
1419
|
+
async getSuggestedChanges(input) {
|
|
1420
|
+
const row = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
|
|
1421
|
+
if (!row) return null;
|
|
1422
|
+
return {
|
|
1423
|
+
runId: row.runId,
|
|
1424
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson))
|
|
1425
|
+
};
|
|
1426
|
+
},
|
|
1427
|
+
async listSuggestedChangesForRun(input) {
|
|
1428
|
+
const rows = await db.select().from(suggestedChanges).where(eq(suggestedChanges.runId, input.runId)).orderBy(asc(suggestedChanges.createdAt));
|
|
1429
|
+
return rows.map((row) => SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson)));
|
|
1430
|
+
},
|
|
1431
|
+
async listLatestSuggestedChangesForConversation(input) {
|
|
1432
|
+
const runRows = await db.select().from(runs).where(eq(runs.conversationKey, input.conversationKey)).orderBy(asc(runs.createdAt));
|
|
1433
|
+
for (const runRow of [...runRows].reverse()) {
|
|
1434
|
+
const proposalRows = await db.select().from(suggestedChanges).where(eq(suggestedChanges.runId, runRow.id)).orderBy(asc(suggestedChanges.createdAt));
|
|
1435
|
+
if (proposalRows.length === 0) continue;
|
|
1436
|
+
const run = runFromRow(runRow);
|
|
1437
|
+
const event = OpenTagEventSchema.parse(JSON.parse(runRow.eventJson));
|
|
1438
|
+
return proposalRows.map((row) => ({
|
|
1439
|
+
runId: row.runId,
|
|
1440
|
+
run,
|
|
1441
|
+
event,
|
|
1442
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson))
|
|
1443
|
+
}));
|
|
1444
|
+
}
|
|
1445
|
+
return [];
|
|
1446
|
+
},
|
|
1447
|
+
async getProposalLineage(input) {
|
|
1448
|
+
const targetRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
|
|
1449
|
+
if (!targetRow) return null;
|
|
1450
|
+
const target = {
|
|
1451
|
+
runId: targetRow.runId,
|
|
1452
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(targetRow.snapshotJson))
|
|
1453
|
+
};
|
|
1454
|
+
const rows = await db.select().from(suggestedChanges).orderBy(asc(suggestedChanges.createdAt));
|
|
1455
|
+
const snapshots = rows.map((row) => ({
|
|
1456
|
+
runId: row.runId,
|
|
1457
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson))
|
|
1458
|
+
}));
|
|
1459
|
+
return computeProposalLineage(snapshots, lineageScopeKey(target));
|
|
1460
|
+
},
|
|
1461
|
+
async listCurrentMutationIntents(input) {
|
|
1462
|
+
const targetRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, input.proposalId)).limit(1).get();
|
|
1463
|
+
if (!targetRow) return null;
|
|
1464
|
+
const rows = await db.select().from(suggestedChanges).orderBy(asc(suggestedChanges.createdAt));
|
|
1465
|
+
const lineage = computeProposalLineage(
|
|
1466
|
+
rows.map((row) => ({
|
|
1467
|
+
runId: row.runId,
|
|
1468
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(row.snapshotJson))
|
|
1469
|
+
})),
|
|
1470
|
+
lineageScopeKey({
|
|
1471
|
+
runId: targetRow.runId,
|
|
1472
|
+
snapshot: SuggestedChangesSnapshotSchema.parse(JSON.parse(targetRow.snapshotJson))
|
|
1473
|
+
})
|
|
1474
|
+
);
|
|
1475
|
+
if (!lineage) return null;
|
|
1476
|
+
return lineage.entries.filter((entry) => entry.status === "current");
|
|
1477
|
+
},
|
|
1478
|
+
async recordApprovalDecision(input) {
|
|
1479
|
+
const decision = ApprovalDecisionSchema.parse(input);
|
|
1480
|
+
const storedProposalRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, decision.proposalId)).limit(1).get();
|
|
1481
|
+
if (!storedProposalRow) return null;
|
|
1482
|
+
await db.insert(approvalDecisions).values({
|
|
1483
|
+
id: decision.id,
|
|
1484
|
+
proposalId: decision.proposalId,
|
|
1485
|
+
decisionJson: JSON.stringify(decision),
|
|
1486
|
+
createdAt: decision.approvedAt
|
|
1487
|
+
}).onConflictDoUpdate({
|
|
1488
|
+
target: approvalDecisions.id,
|
|
1489
|
+
set: {
|
|
1490
|
+
proposalId: decision.proposalId,
|
|
1491
|
+
decisionJson: JSON.stringify(decision),
|
|
1492
|
+
createdAt: decision.approvedAt
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
await appendRunEvent({
|
|
1496
|
+
runId: storedProposalRow.runId,
|
|
1497
|
+
type: "approval.decision.recorded",
|
|
1498
|
+
payload: decision,
|
|
1499
|
+
visibility: "audit",
|
|
1500
|
+
importance: "high",
|
|
1501
|
+
message: `Approved ${decision.approvedIntentIds.length} intent(s).`,
|
|
1502
|
+
createdAt: decision.approvedAt
|
|
1503
|
+
});
|
|
1504
|
+
await appendRunEvent({
|
|
1505
|
+
runId: storedProposalRow.runId,
|
|
1506
|
+
type: "success_metric.observed",
|
|
1507
|
+
payload: {
|
|
1508
|
+
metric: "external_write_approval_rate",
|
|
1509
|
+
proposalId: decision.proposalId,
|
|
1510
|
+
approvedIntentCount: decision.approvedIntentIds.length
|
|
1511
|
+
},
|
|
1512
|
+
visibility: "audit",
|
|
1513
|
+
importance: "normal",
|
|
1514
|
+
createdAt: decision.approvedAt
|
|
1515
|
+
});
|
|
1516
|
+
return decision;
|
|
1517
|
+
},
|
|
1518
|
+
async getApprovalDecision(input) {
|
|
1519
|
+
const row = await db.select().from(approvalDecisions).where(eq(approvalDecisions.id, input.id)).limit(1).get();
|
|
1520
|
+
return row ? ApprovalDecisionSchema.parse(JSON.parse(row.decisionJson)) : null;
|
|
1521
|
+
},
|
|
1522
|
+
async createApplyPlan(input) {
|
|
1523
|
+
const built = await buildApplyPlan(input);
|
|
1524
|
+
if (!built) return null;
|
|
1525
|
+
await db.insert(applyPlans).values({
|
|
1526
|
+
id: built.plan.id,
|
|
1527
|
+
proposalId: built.plan.proposalId,
|
|
1528
|
+
approvalDecisionId: built.plan.approvalDecisionId,
|
|
1529
|
+
planJson: JSON.stringify(built.plan),
|
|
1530
|
+
createdAt: built.createdAt
|
|
1531
|
+
}).onConflictDoUpdate({
|
|
1532
|
+
target: applyPlans.id,
|
|
1533
|
+
set: {
|
|
1534
|
+
proposalId: built.plan.proposalId,
|
|
1535
|
+
approvalDecisionId: built.plan.approvalDecisionId,
|
|
1536
|
+
planJson: JSON.stringify(built.plan),
|
|
1537
|
+
createdAt: built.createdAt
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
await appendApplyPlanCreatedEvent(built);
|
|
1541
|
+
return built.plan;
|
|
1542
|
+
},
|
|
1543
|
+
async createApplyPlanOnce(input) {
|
|
1544
|
+
const built = await buildApplyPlan(input);
|
|
1545
|
+
if (!built) return null;
|
|
1546
|
+
const result = db.transaction((tx) => {
|
|
1547
|
+
const insertResult = tx.insert(applyPlans).values({
|
|
1548
|
+
id: built.plan.id,
|
|
1549
|
+
proposalId: built.plan.proposalId,
|
|
1550
|
+
approvalDecisionId: built.plan.approvalDecisionId,
|
|
1551
|
+
planJson: JSON.stringify(built.plan),
|
|
1552
|
+
createdAt: built.createdAt
|
|
1553
|
+
}).onConflictDoNothing({ target: applyPlans.id }).run();
|
|
1554
|
+
if (insertResult.changes === 0) {
|
|
1555
|
+
return { created: false };
|
|
1556
|
+
}
|
|
1557
|
+
tx.insert(runEvents).values(applyPlanCreatedEventRow(built)).run();
|
|
1558
|
+
return { created: true };
|
|
1559
|
+
});
|
|
1560
|
+
if (!result.created) {
|
|
1561
|
+
const existing = await db.select().from(applyPlans).where(eq(applyPlans.id, input.id)).limit(1).get();
|
|
1562
|
+
if (!existing) {
|
|
1563
|
+
throw new Error(`Apply plan ${input.id} already exists but could not be loaded`);
|
|
1564
|
+
}
|
|
1565
|
+
return { plan: ApplyPlanSchema.parse(JSON.parse(existing.planJson)), created: false };
|
|
1566
|
+
}
|
|
1567
|
+
return { plan: built.plan, created: true };
|
|
1568
|
+
},
|
|
1569
|
+
async getApplyPlan(input) {
|
|
1570
|
+
const row = await db.select().from(applyPlans).where(eq(applyPlans.id, input.id)).limit(1).get();
|
|
1571
|
+
return row ? ApplyPlanSchema.parse(JSON.parse(row.planJson)) : null;
|
|
1572
|
+
},
|
|
1573
|
+
async updateApplyPlanOutcomes(input) {
|
|
1574
|
+
const row = await db.select().from(applyPlans).where(eq(applyPlans.id, input.id)).limit(1).get();
|
|
1575
|
+
if (!row) return null;
|
|
1576
|
+
const currentPlan = ApplyPlanSchema.parse(JSON.parse(row.planJson));
|
|
1577
|
+
const outcomes = input.outcomes.map((outcome) => ApplyIntentOutcomeSchema.parse(outcome));
|
|
1578
|
+
const updatedPlan = ApplyPlanSchema.parse({
|
|
1579
|
+
...currentPlan,
|
|
1580
|
+
adapterPlan: {
|
|
1581
|
+
...currentPlan.adapterPlan && typeof currentPlan.adapterPlan === "object" && !Array.isArray(currentPlan.adapterPlan) ? currentPlan.adapterPlan : {},
|
|
1582
|
+
externalWritesExecuted: input.externalWritesExecuted
|
|
1583
|
+
},
|
|
1584
|
+
outcomes
|
|
1585
|
+
});
|
|
1586
|
+
const updatedAt = nowIso();
|
|
1587
|
+
await db.update(applyPlans).set({ planJson: JSON.stringify(updatedPlan), createdAt: row.createdAt }).where(eq(applyPlans.id, input.id));
|
|
1588
|
+
const storedProposalRow = await db.select().from(suggestedChanges).where(eq(suggestedChanges.proposalId, updatedPlan.proposalId)).limit(1).get();
|
|
1589
|
+
if (storedProposalRow) {
|
|
1590
|
+
await appendRunEvent({
|
|
1591
|
+
runId: storedProposalRow.runId,
|
|
1592
|
+
type: "apply_plan.executed",
|
|
1593
|
+
payload: updatedPlan,
|
|
1594
|
+
visibility: "audit",
|
|
1595
|
+
importance: "high",
|
|
1596
|
+
message: `Executed apply plan with ${outcomes.length} outcome(s).`,
|
|
1597
|
+
createdAt: updatedAt
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
return updatedPlan;
|
|
379
1601
|
},
|
|
380
1602
|
async recordProgress(input) {
|
|
1603
|
+
if (input.runnerId) {
|
|
1604
|
+
const row = await db.select().from(runs).where(and(eq(runs.id, input.runId), eq(runs.assignedRunnerId, input.runnerId))).limit(1).get();
|
|
1605
|
+
if (!row) return false;
|
|
1606
|
+
}
|
|
381
1607
|
await appendRunEvent({
|
|
382
1608
|
runId: input.runId,
|
|
383
1609
|
type: "run.progress",
|
|
384
1610
|
payload: {
|
|
1611
|
+
...input.runnerId ? { runnerId: input.runnerId } : {},
|
|
385
1612
|
type: input.type ?? "progress",
|
|
386
1613
|
message: input.message,
|
|
387
1614
|
at: input.at ?? nowIso()
|
|
388
|
-
}
|
|
1615
|
+
},
|
|
1616
|
+
visibility: input.visibility ?? "audit",
|
|
1617
|
+
importance: input.importance ?? "normal",
|
|
1618
|
+
message: input.message,
|
|
1619
|
+
createdAt: input.at ?? nowIso()
|
|
389
1620
|
});
|
|
1621
|
+
return true;
|
|
390
1622
|
},
|
|
391
1623
|
async getRun(input) {
|
|
392
1624
|
const row = await db.select().from(runs).where(eq(runs.id, input.runId)).limit(1).get();
|
|
@@ -402,19 +1634,196 @@ function createOpenTagRepository(db) {
|
|
|
402
1634
|
id: row.id,
|
|
403
1635
|
runId: row.runId,
|
|
404
1636
|
type: row.type,
|
|
1637
|
+
visibility: RunEventVisibilitySchema.parse(row.visibility),
|
|
1638
|
+
importance: RunEventImportanceSchema.parse(row.importance),
|
|
1639
|
+
...row.message ? { message: row.message } : {},
|
|
1640
|
+
payload: JSON.parse(row.payloadJson),
|
|
1641
|
+
createdAt: row.createdAt
|
|
1642
|
+
}));
|
|
1643
|
+
},
|
|
1644
|
+
async enqueueCallbackDelivery(input) {
|
|
1645
|
+
const createdAt = nowIso();
|
|
1646
|
+
const rows = await db.insert(callbackDeliveries).values({
|
|
1647
|
+
runId: input.runId,
|
|
1648
|
+
kind: input.kind,
|
|
1649
|
+
provider: input.provider,
|
|
1650
|
+
uri: input.uri,
|
|
1651
|
+
body: input.body,
|
|
1652
|
+
threadKey: input.threadKey ?? null,
|
|
1653
|
+
metadataJson: JSON.stringify({
|
|
1654
|
+
...input.agentId ? { agentId: input.agentId } : {},
|
|
1655
|
+
...input.statusMessageKey ? { statusMessageKey: input.statusMessageKey } : {},
|
|
1656
|
+
...input.blocks ? { blocks: input.blocks } : {}
|
|
1657
|
+
}),
|
|
1658
|
+
status: "pending",
|
|
1659
|
+
createdAt,
|
|
1660
|
+
updatedAt: createdAt
|
|
1661
|
+
}).returning();
|
|
1662
|
+
const row = rows[0];
|
|
1663
|
+
if (!row) throw new Error("callback delivery was not created");
|
|
1664
|
+
await appendRunEvent({
|
|
1665
|
+
runId: input.runId,
|
|
1666
|
+
type: `callback.${input.kind}.queued`,
|
|
1667
|
+
payload: callbackDeliveryFromRow(row),
|
|
1668
|
+
visibility: "audit",
|
|
1669
|
+
importance: "normal",
|
|
1670
|
+
createdAt
|
|
1671
|
+
});
|
|
1672
|
+
return callbackDeliveryFromRow(row);
|
|
1673
|
+
},
|
|
1674
|
+
async markCallbackDelivered(input) {
|
|
1675
|
+
const updatedAt = nowIso();
|
|
1676
|
+
const row = await db.select().from(callbackDeliveries).where(eq(callbackDeliveries.id, input.deliveryId)).limit(1).get();
|
|
1677
|
+
if (!row) return;
|
|
1678
|
+
await db.update(callbackDeliveries).set({ status: "delivered", attempts: row.attempts + 1, lastError: null, nextAttemptAt: null, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
|
|
1679
|
+
await appendRunEvent({
|
|
1680
|
+
runId: row.runId,
|
|
1681
|
+
type: `callback.${row.kind}.delivered`,
|
|
1682
|
+
payload: { ...callbackDeliveryFromRow(row), status: "delivered", attempts: row.attempts + 1, updatedAt },
|
|
1683
|
+
visibility: "human",
|
|
1684
|
+
importance: row.kind === "final" ? "high" : "normal",
|
|
1685
|
+
message: row.body,
|
|
1686
|
+
createdAt: updatedAt
|
|
1687
|
+
});
|
|
1688
|
+
},
|
|
1689
|
+
async markCallbackFailed(input) {
|
|
1690
|
+
const updatedAt = nowIso();
|
|
1691
|
+
const row = await db.select().from(callbackDeliveries).where(eq(callbackDeliveries.id, input.deliveryId)).limit(1).get();
|
|
1692
|
+
if (!row) return;
|
|
1693
|
+
await db.update(callbackDeliveries).set({ status: "failed", attempts: row.attempts + 1, lastError: input.error, nextAttemptAt: input.nextAttemptAt ?? null, updatedAt }).where(eq(callbackDeliveries.id, input.deliveryId));
|
|
1694
|
+
await appendRunEvent({
|
|
1695
|
+
runId: row.runId,
|
|
1696
|
+
type: `callback.${row.kind}.failed`,
|
|
1697
|
+
payload: {
|
|
1698
|
+
...callbackDeliveryFromRow(row),
|
|
1699
|
+
status: "failed",
|
|
1700
|
+
attempts: row.attempts + 1,
|
|
1701
|
+
lastError: input.error,
|
|
1702
|
+
...input.nextAttemptAt ? { nextAttemptAt: input.nextAttemptAt } : {},
|
|
1703
|
+
updatedAt
|
|
1704
|
+
},
|
|
1705
|
+
visibility: "audit",
|
|
1706
|
+
importance: "normal",
|
|
1707
|
+
createdAt: updatedAt
|
|
1708
|
+
});
|
|
1709
|
+
},
|
|
1710
|
+
async listPendingCallbackDeliveries(input) {
|
|
1711
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1712
|
+
const maxAttempts = input.maxAttempts ?? Number.POSITIVE_INFINITY;
|
|
1713
|
+
const rows = await db.select().from(callbackDeliveries).where(inArray(callbackDeliveries.status, ["pending", "failed"])).orderBy(asc(callbackDeliveries.id));
|
|
1714
|
+
return rows.map(callbackDeliveryFromRow).filter((delivery) => delivery.attempts < maxAttempts).filter((delivery) => !delivery.nextAttemptAt || new Date(delivery.nextAttemptAt).getTime() <= now.getTime()).slice(0, input.limit);
|
|
1715
|
+
},
|
|
1716
|
+
async claimPendingCallbackDeliveries(input) {
|
|
1717
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1718
|
+
const maxAttempts = input.maxAttempts ?? Number.POSITIVE_INFINITY;
|
|
1719
|
+
const staleThresholdMs = input.staleDeliveryThresholdMs ?? 6e4;
|
|
1720
|
+
const staleDeliveryCutoff = new Date(now.getTime() - staleThresholdMs).toISOString();
|
|
1721
|
+
const rows = await db.select().from(callbackDeliveries).where(inArray(callbackDeliveries.status, ["pending", "failed", "delivering"])).orderBy(asc(callbackDeliveries.id));
|
|
1722
|
+
const claimed = [];
|
|
1723
|
+
for (const row of rows) {
|
|
1724
|
+
const delivery = callbackDeliveryFromRow(row);
|
|
1725
|
+
if (delivery.attempts >= maxAttempts) continue;
|
|
1726
|
+
if (delivery.nextAttemptAt && new Date(delivery.nextAttemptAt).getTime() > now.getTime()) continue;
|
|
1727
|
+
if (row.status === "delivering" && row.updatedAt > staleDeliveryCutoff) continue;
|
|
1728
|
+
const updatedAt = input.now ? input.now.toISOString() : nowIso();
|
|
1729
|
+
const claimWhere = row.status === "delivering" ? and(eq(callbackDeliveries.id, row.id), eq(callbackDeliveries.status, "delivering"), eq(callbackDeliveries.updatedAt, row.updatedAt)) : and(eq(callbackDeliveries.id, row.id), inArray(callbackDeliveries.status, ["pending", "failed"]));
|
|
1730
|
+
const claimResult = await db.update(callbackDeliveries).set({ status: "delivering", updatedAt }).where(claimWhere);
|
|
1731
|
+
if (claimResult.changes === 0) continue;
|
|
1732
|
+
claimed.push({
|
|
1733
|
+
...delivery,
|
|
1734
|
+
status: "delivering",
|
|
1735
|
+
updatedAt
|
|
1736
|
+
});
|
|
1737
|
+
if (claimed.length >= input.limit) break;
|
|
1738
|
+
}
|
|
1739
|
+
return claimed;
|
|
1740
|
+
},
|
|
1741
|
+
async getRunMetrics(input) {
|
|
1742
|
+
const rows = await db.select().from(runEvents).where(eq(runEvents.runId, input.runId)).orderBy(asc(runEvents.id));
|
|
1743
|
+
const events = rows.map((row) => ({
|
|
1744
|
+
id: row.id,
|
|
1745
|
+
runId: row.runId,
|
|
1746
|
+
type: row.type,
|
|
1747
|
+
visibility: RunEventVisibilitySchema.parse(row.visibility),
|
|
1748
|
+
importance: RunEventImportanceSchema.parse(row.importance),
|
|
1749
|
+
...row.message ? { message: row.message } : {},
|
|
405
1750
|
payload: JSON.parse(row.payloadJson),
|
|
406
1751
|
createdAt: row.createdAt
|
|
407
1752
|
}));
|
|
1753
|
+
return metricsFromEvents(input.runId, events);
|
|
1754
|
+
},
|
|
1755
|
+
async getRepoMetrics(input) {
|
|
1756
|
+
const runRows = await db.select().from(runs).where(and(eq(runs.repoProvider, input.provider), eq(runs.repoOwner, input.owner), eq(runs.repoName, input.repo))).orderBy(asc(runs.createdAt));
|
|
1757
|
+
const matchingRunIds = runRows.map((row) => row.id);
|
|
1758
|
+
const runMetrics = [];
|
|
1759
|
+
for (const runId of matchingRunIds) {
|
|
1760
|
+
const rows = await db.select().from(runEvents).where(eq(runEvents.runId, runId)).orderBy(asc(runEvents.id));
|
|
1761
|
+
runMetrics.push(
|
|
1762
|
+
metricsFromEvents(
|
|
1763
|
+
runId,
|
|
1764
|
+
rows.map((row) => ({
|
|
1765
|
+
id: row.id,
|
|
1766
|
+
runId: row.runId,
|
|
1767
|
+
type: row.type,
|
|
1768
|
+
visibility: RunEventVisibilitySchema.parse(row.visibility),
|
|
1769
|
+
importance: RunEventImportanceSchema.parse(row.importance),
|
|
1770
|
+
...row.message ? { message: row.message } : {},
|
|
1771
|
+
payload: JSON.parse(row.payloadJson),
|
|
1772
|
+
createdAt: row.createdAt
|
|
1773
|
+
}))
|
|
1774
|
+
)
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
return aggregateMetrics({
|
|
1778
|
+
scope: "repo",
|
|
1779
|
+
scopeId: `${input.provider}:${input.owner}/${input.repo}`,
|
|
1780
|
+
runs: runMetrics
|
|
1781
|
+
});
|
|
1782
|
+
},
|
|
1783
|
+
async getWorkThreadMetrics(input) {
|
|
1784
|
+
const runRows = await db.select().from(runs).where(eq(runs.workThreadId, input.threadId)).orderBy(asc(runs.createdAt));
|
|
1785
|
+
const matchingRunIds = runRows.map((row) => row.id);
|
|
1786
|
+
const runMetrics = [];
|
|
1787
|
+
for (const runId of matchingRunIds) {
|
|
1788
|
+
const rows = await db.select().from(runEvents).where(eq(runEvents.runId, runId)).orderBy(asc(runEvents.id));
|
|
1789
|
+
runMetrics.push(
|
|
1790
|
+
metricsFromEvents(
|
|
1791
|
+
runId,
|
|
1792
|
+
rows.map((row) => ({
|
|
1793
|
+
id: row.id,
|
|
1794
|
+
runId: row.runId,
|
|
1795
|
+
type: row.type,
|
|
1796
|
+
visibility: RunEventVisibilitySchema.parse(row.visibility),
|
|
1797
|
+
importance: RunEventImportanceSchema.parse(row.importance),
|
|
1798
|
+
...row.message ? { message: row.message } : {},
|
|
1799
|
+
payload: JSON.parse(row.payloadJson),
|
|
1800
|
+
createdAt: row.createdAt
|
|
1801
|
+
}))
|
|
1802
|
+
)
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
return aggregateMetrics({
|
|
1806
|
+
scope: "work_thread",
|
|
1807
|
+
scopeId: input.threadId,
|
|
1808
|
+
runs: runMetrics
|
|
1809
|
+
});
|
|
408
1810
|
}
|
|
409
1811
|
};
|
|
410
1812
|
}
|
|
411
1813
|
export {
|
|
1814
|
+
applyPlans,
|
|
1815
|
+
approvalDecisions,
|
|
1816
|
+
callbackDeliveries,
|
|
1817
|
+
channelBindings,
|
|
412
1818
|
createOpenTagRepository,
|
|
1819
|
+
followUpRequests,
|
|
413
1820
|
migrateSchema,
|
|
414
1821
|
repoBindings,
|
|
1822
|
+
repoMutationMappings,
|
|
1823
|
+
repoPolicyRules,
|
|
415
1824
|
runEvents,
|
|
416
1825
|
runners,
|
|
417
1826
|
runs,
|
|
418
|
-
|
|
1827
|
+
suggestedChanges
|
|
419
1828
|
};
|
|
420
1829
|
//# sourceMappingURL=index.js.map
|