@opentag/store 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,9 +1,29 @@
1
1
  // src/repository.ts
2
- import { OpenTagEventSchema, OpenTagRunResultSchema } from "@opentag/core";
3
- import { and, asc, eq } from "drizzle-orm";
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 runEvents = sqliteTable("run_events", {
29
- id: integer("id").primaryKey({ autoIncrement: true }),
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
- type: text("type").notNull(),
32
- payloadJson: text("payload_json").notNull(),
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 slackChannelBindings = sqliteTable(
59
- "slack_channel_bindings",
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
- teamId: text("team_id").notNull(),
63
- channelId: text("channel_id").notNull(),
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
- slackChannelUniqueIdx: uniqueIndex("slack_channel_bindings_team_channel_idx").on(table.teamId, table.channelId)
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 slack_channel_bindings (
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
- team_id TEXT NOT NULL,
119
- channel_id TEXT NOT NULL,
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 slack_channel_bindings_team_channel_idx
125
- ON slack_channel_bindings(team_id, channel_id);
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 repoKeyFromEvent(event) {
170
- const owner = event.metadata["owner"];
171
- const repo = event.metadata["repo"];
172
- if (typeof owner !== "string" || typeof repo !== "string") return null;
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
- provider: typeof event.metadata["repoProvider"] === "string" ? event.metadata["repoProvider"] : "github",
175
- owner,
176
- repo
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
- await db.insert(slackChannelBindings).values({
213
- teamId: input.teamId,
214
- channelId: input.channelId,
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: [slackChannelBindings.teamId, slackChannelBindings.channelId],
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
- await db.insert(runs).values({
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
- id: input.id,
245
- eventId: event.id,
246
- status: "queued",
247
- createdAt,
248
- updatedAt: createdAt
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(and(eq(runs.status, "assigned"))).orderBy(asc(runs.createdAt));
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 = repoKeyFromEvent(event);
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
- id: row.id,
308
- eventId: row.eventId,
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(slackChannelBindings).where(and(eq(slackChannelBindings.teamId, input.teamId), eq(slackChannelBindings.channelId, input.channelId))).limit(1).get();
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: row.teamId,
338
- channelId: row.channelId,
339
- owner: row.owner,
340
- repo: row.repo
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
- await db.update(runs).set({ status: "running", executor: input.executor, updatedAt }).where(eq(runs.id, input.runId));
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.update(runs).set({ status, resultJson: JSON.stringify(result), updatedAt }).where(eq(runs.id, input.runId));
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
- slackChannelBindings
1827
+ suggestedChanges
419
1828
  };
420
1829
  //# sourceMappingURL=index.js.map