conductor-remote 1.106.3 → 1.108.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.
@@ -0,0 +1,417 @@
1
+ import { getTableName, sql } from 'drizzle-orm';
2
+ import { check, customType, index, integer, primaryKey, sqliteTable, text, unique, uniqueIndex } from 'drizzle-orm/sqlite-core';
3
+ import { createSelectSchema, jsonSchema } from 'drizzle-orm/zod';
4
+ import { z } from 'zod';
5
+ export { ORCHESTRATION_BOOTSTRAP_SQL } from "./orchestration-schema.generated.js";
6
+ export const ORCHESTRATION_SCHEMA_VERSION = 1;
7
+ export const WORKFLOW_PHASES = [
8
+ 'creating_workspace',
9
+ 'binding_root',
10
+ 'pending_root',
11
+ 'exploring',
12
+ 'planning',
13
+ 'implementing',
14
+ 'reviewing',
15
+ 'blocked',
16
+ 'completed',
17
+ 'cancelled'
18
+ ];
19
+ export const RESUMABLE_WORKFLOW_PHASES = [
20
+ 'creating_workspace',
21
+ 'binding_root',
22
+ 'pending_root',
23
+ 'exploring',
24
+ 'planning',
25
+ 'implementing',
26
+ 'reviewing'
27
+ ];
28
+ export const WORKFLOW_JOB_ROLES = ['exploration', 'implementation'];
29
+ export const WORKFLOW_JOB_STATES = [
30
+ 'dormant',
31
+ 'queued',
32
+ 'owned',
33
+ 'opening',
34
+ 'configuring',
35
+ 'sending',
36
+ 'running',
37
+ 'returning',
38
+ 'returned',
39
+ 'failed',
40
+ 'cancelled'
41
+ ];
42
+ export const WORKFLOW_EFFECT_STATES = [
43
+ 'prepared',
44
+ 'dispatched',
45
+ 'committed',
46
+ 'failed',
47
+ 'ambiguous',
48
+ 'cancelled'
49
+ ];
50
+ export const WORKFLOW_RETRY_CLASSES = ['deterministic', 'ambiguous', 'terminal'];
51
+ const agentEfforts = ['none', 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
52
+ const workflowRoleNames = ['planning', 'exploration', 'implementation'];
53
+ export const workflowTargetSchema = z.discriminatedUnion('kind', [
54
+ z.object({ kind: z.literal('new_workspace'), repo: z.string(), sendImmediately: z.boolean() }),
55
+ z.object({ kind: z.literal('existing_session'), workspaceId: z.string(), sessionId: z.string() })
56
+ ]);
57
+ export const resolvedWorkflowRoleSchema = z.object({
58
+ model: z.string(),
59
+ agentType: z.string(),
60
+ effort: z.enum(agentEfforts).optional(),
61
+ fast: z.boolean().optional(),
62
+ preamble: z.string().optional()
63
+ });
64
+ export const frozenWorkflowRolesSchema = z.object(Object.fromEntries(workflowRoleNames.map(role => [role, resolvedWorkflowRoleSchema])));
65
+ export const workflowAdoptionCandidateSchema = z.object({
66
+ id: z.string(),
67
+ title: z.string(),
68
+ repo: z.string(),
69
+ createdAt: z.number(),
70
+ kind: z.enum(['workspace', 'session']).optional()
71
+ });
72
+ const relayOwnerColumns = () => ({
73
+ ownerInstanceId: text('owner_instance_id'),
74
+ ownerPid: integer('owner_pid'),
75
+ ownerProcessStartedAt: text('owner_process_started_at'),
76
+ ownerProtocolVersion: integer('owner_protocol_version')
77
+ });
78
+ const externalProcessColumns = () => ({
79
+ externalPid: integer('external_pid'),
80
+ externalProcessStartedAt: text('external_process_started_at'),
81
+ externalProcessGroup: integer('external_process_group')
82
+ });
83
+ const timestamps = () => ({
84
+ createdAt: integer('created_at').notNull(),
85
+ updatedAt: integer('updated_at').notNull(),
86
+ terminalAt: integer('terminal_at')
87
+ });
88
+ const jsonColumn = (name) => text(name, { mode: 'json' }).$type();
89
+ const optionalJsonColumn = (name) => customType({
90
+ dataType: () => 'text',
91
+ fromDriver: value => ({ value: JSON.parse(value) }),
92
+ toDriver: stored => JSON.stringify(stored.value)
93
+ })(name);
94
+ const requiredJson = (name, column) => check(name, sql `json_valid(${column})`);
95
+ const optionalJson = (name, column) => check(name, sql `${column} IS NULL OR json_valid(${column})`);
96
+ const enumCheck = (column, values) => sql `${column} IN (${sql.raw(values.map(value => `'${value}'`).join(', '))})`;
97
+ export const orchestrationMeta = sqliteTable('orchestration_meta', {
98
+ key: text().primaryKey(),
99
+ value: text().notNull()
100
+ });
101
+ export const workflowRuns = sqliteTable('workflow_runs', {
102
+ id: text().primaryKey(),
103
+ objective: text().notNull(),
104
+ target: jsonColumn('target_json').notNull(),
105
+ roles: jsonColumn('roles_json').notNull(),
106
+ phase: text({ enum: WORKFLOW_PHASES }).notNull(),
107
+ cycle: integer().notNull().default(0),
108
+ revision: integer().notNull().default(0),
109
+ workspaceId: text('workspace_id'),
110
+ rootSessionId: text('root_session_id'),
111
+ pristineEvidence: optionalJsonColumn('pristine_evidence_json'),
112
+ deliveryBaseline: optionalJsonColumn('delivery_baseline_json'),
113
+ planningInterpretation: text('planning_interpretation'),
114
+ cancellationGeneration: integer('cancellation_generation').notNull().default(0),
115
+ blockedActionId: text('blocked_action_id'),
116
+ blockedErrorCode: text('blocked_error_code'),
117
+ blockedMessage: text('blocked_message'),
118
+ resumePhase: text('resume_phase', { enum: RESUMABLE_WORKFLOW_PHASES }),
119
+ retryClass: text('retry_class', { enum: WORKFLOW_RETRY_CLASSES }),
120
+ blockedCandidates: optionalJsonColumn('blocked_candidates_json'),
121
+ blockedAt: integer('blocked_at'),
122
+ implementationBatonsDelivered: integer('implementation_batons_delivered').notNull().default(0),
123
+ ...timestamps()
124
+ }, table => [
125
+ requiredJson('workflow_runs_target_json_valid', table.target),
126
+ requiredJson('workflow_runs_roles_json_valid', table.roles),
127
+ check('workflow_runs_phase_valid', enumCheck(table.phase, WORKFLOW_PHASES)),
128
+ check('workflow_runs_cycle_nonnegative', sql `${table.cycle} >= 0`),
129
+ check('workflow_runs_revision_nonnegative', sql `${table.revision} >= 0`),
130
+ optionalJson('workflow_runs_pristine_evidence_json_valid', table.pristineEvidence),
131
+ optionalJson('workflow_runs_delivery_baseline_json_valid', table.deliveryBaseline),
132
+ check('workflow_runs_cancellation_generation_nonnegative', sql `${table.cancellationGeneration} >= 0`),
133
+ check('workflow_runs_resume_phase_valid', sql `${table.resumePhase} IS NULL OR ${enumCheck(table.resumePhase, RESUMABLE_WORKFLOW_PHASES)}`),
134
+ check('workflow_runs_retry_class_valid', sql `${table.retryClass} IS NULL OR ${enumCheck(table.retryClass, WORKFLOW_RETRY_CLASSES)}`),
135
+ optionalJson('workflow_runs_blocked_candidates_json_valid', table.blockedCandidates),
136
+ check('workflow_runs_implementation_batons_nonnegative', sql `${table.implementationBatonsDelivered} >= 0`),
137
+ uniqueIndex('workflow_runs_one_active_root')
138
+ .on(table.rootSessionId)
139
+ .where(sql `${table.rootSessionId} IS NOT NULL AND ${table.phase} NOT IN ('completed', 'cancelled')`),
140
+ index('workflow_runs_active_updated')
141
+ .on(table.phase, table.updatedAt)
142
+ .where(sql `${table.phase} NOT IN ('completed', 'cancelled')`),
143
+ index('workflow_runs_workspace_active')
144
+ .on(table.workspaceId, table.phase)
145
+ .where(sql `${table.phase} NOT IN ('completed', 'cancelled')`)
146
+ ]);
147
+ export const workflowCapabilities = sqliteTable('workflow_capabilities', {
148
+ id: text().primaryKey(),
149
+ tokenHash: text('token_hash').notNull().unique(),
150
+ runId: text('run_id')
151
+ .notNull()
152
+ .references(() => workflowRuns.id, { onDelete: 'cascade' }),
153
+ rootSessionId: text('root_session_id').notNull(),
154
+ cycle: integer().notNull(),
155
+ phase: text({ enum: WORKFLOW_PHASES }).notNull(),
156
+ revision: integer().notNull(),
157
+ allowedRoles: jsonColumn('allowed_roles_json').notNull(),
158
+ issuedWithRowid: integer('issued_with_rowid'),
159
+ issuedWithTurnId: text('issued_with_turn_id'),
160
+ consumedAt: integer('consumed_at'),
161
+ revokedAt: integer('revoked_at'),
162
+ createdAt: integer('created_at').notNull()
163
+ }, table => [
164
+ check('workflow_capabilities_token_hash_length', sql `length(${table.tokenHash}) = 64`),
165
+ check('workflow_capabilities_cycle_nonnegative', sql `${table.cycle} >= 0`),
166
+ check('workflow_capabilities_revision_nonnegative', sql `${table.revision} >= 0`),
167
+ requiredJson('workflow_capabilities_allowed_roles_json_valid', table.allowedRoles),
168
+ index('workflow_capabilities_current')
169
+ .on(table.runId, table.phase, table.cycle, table.revision)
170
+ .where(sql `${table.consumedAt} IS NULL AND ${table.revokedAt} IS NULL`)
171
+ ]);
172
+ export const workflowJobs = sqliteTable('workflow_jobs', {
173
+ id: text().primaryKey(),
174
+ runId: text('run_id')
175
+ .notNull()
176
+ .references(() => workflowRuns.id, { onDelete: 'cascade' }),
177
+ logicalKey: text('logical_key').notNull(),
178
+ role: text({ enum: WORKFLOW_JOB_ROLES }).notNull(),
179
+ cycle: integer().notNull(),
180
+ revision: integer().notNull(),
181
+ resolvedRole: jsonColumn('resolved_role_json').notNull(),
182
+ prompt: text().notNull(),
183
+ state: text({ enum: WORKFLOW_JOB_STATES }).notNull(),
184
+ cancellationGeneration: integer('cancellation_generation').notNull(),
185
+ ...relayOwnerColumns(),
186
+ transcriptCursor: optionalJsonColumn('transcript_cursor_json'),
187
+ childSessionId: text('child_session_id'),
188
+ outcome: optionalJsonColumn('outcome_json'),
189
+ taskReceipt: optionalJsonColumn('task_receipt_json'),
190
+ batonReceipt: optionalJsonColumn('baton_receipt_json'),
191
+ attemptCount: integer('attempt_count').notNull().default(0),
192
+ ...timestamps()
193
+ }, table => [
194
+ check('workflow_jobs_role_valid', enumCheck(table.role, WORKFLOW_JOB_ROLES)),
195
+ check('workflow_jobs_cycle_nonnegative', sql `${table.cycle} >= 0`),
196
+ check('workflow_jobs_revision_nonnegative', sql `${table.revision} >= 0`),
197
+ requiredJson('workflow_jobs_resolved_role_json_valid', table.resolvedRole),
198
+ check('workflow_jobs_state_valid', enumCheck(table.state, WORKFLOW_JOB_STATES)),
199
+ check('workflow_jobs_cancellation_generation_nonnegative', sql `${table.cancellationGeneration} >= 0`),
200
+ optionalJson('workflow_jobs_transcript_cursor_json_valid', table.transcriptCursor),
201
+ optionalJson('workflow_jobs_outcome_json_valid', table.outcome),
202
+ optionalJson('workflow_jobs_task_receipt_json_valid', table.taskReceipt),
203
+ optionalJson('workflow_jobs_baton_receipt_json_valid', table.batonReceipt),
204
+ check('workflow_jobs_attempt_count_nonnegative', sql `${table.attemptCount} >= 0`),
205
+ unique('workflow_jobs_run_logical_key_unique').on(table.runId, table.logicalKey),
206
+ index('workflow_jobs_runnable').on(table.state, table.createdAt).where(sql `${table.state} = 'queued'`),
207
+ index('workflow_jobs_run_state').on(table.runId, table.role, table.state)
208
+ ]);
209
+ export const workflowJobAttempts = sqliteTable('workflow_job_attempts', {
210
+ id: text().primaryKey(),
211
+ jobId: text('job_id')
212
+ .notNull()
213
+ .references(() => workflowJobs.id, { onDelete: 'cascade' }),
214
+ attemptNumber: integer('attempt_number').notNull(),
215
+ state: text({ enum: WORKFLOW_JOB_STATES }).notNull(),
216
+ childSessionId: text('child_session_id'),
217
+ openEffectId: text('open_effect_id'),
218
+ configureEffectId: text('configure_effect_id'),
219
+ taskEffectId: text('task_effect_id'),
220
+ batonEffectId: text('baton_effect_id'),
221
+ outcome: optionalJsonColumn('outcome_json'),
222
+ failureEvidence: optionalJsonColumn('failure_evidence_json'),
223
+ ...relayOwnerColumns(),
224
+ ...timestamps()
225
+ }, table => [
226
+ check('workflow_job_attempts_attempt_positive', sql `${table.attemptNumber} > 0`),
227
+ check('workflow_job_attempts_state_valid', enumCheck(table.state, WORKFLOW_JOB_STATES)),
228
+ optionalJson('workflow_job_attempts_outcome_json_valid', table.outcome),
229
+ optionalJson('workflow_job_attempts_failure_evidence_json_valid', table.failureEvidence),
230
+ unique('workflow_job_attempts_job_number_unique').on(table.jobId, table.attemptNumber)
231
+ ]);
232
+ export const workflowEffects = sqliteTable('workflow_effects', {
233
+ id: text().primaryKey(),
234
+ runId: text('run_id')
235
+ .notNull()
236
+ .references(() => workflowRuns.id, { onDelete: 'cascade' }),
237
+ actionId: text('action_id').notNull(),
238
+ jobId: text('job_id').references(() => workflowJobs.id, { onDelete: 'cascade' }),
239
+ kind: text().notNull(),
240
+ state: text({ enum: WORKFLOW_EFFECT_STATES }).notNull(),
241
+ target: optionalJsonColumn('target_json'),
242
+ inputs: optionalJsonColumn('inputs_json'),
243
+ baseline: optionalJsonColumn('baseline_json'),
244
+ cursor: optionalJsonColumn('cursor_json'),
245
+ receipt: optionalJsonColumn('receipt_json'),
246
+ errorCode: text('error_code'),
247
+ errorMessage: text('error_message'),
248
+ ...relayOwnerColumns(),
249
+ launchNonce: text('launch_nonce'),
250
+ ...externalProcessColumns(),
251
+ mayExecute: integer('may_execute', { mode: 'boolean' }).notNull().default(false),
252
+ attemptCount: integer('attempt_count').notNull().default(0),
253
+ ...timestamps()
254
+ }, table => [
255
+ check('workflow_effects_state_valid', enumCheck(table.state, WORKFLOW_EFFECT_STATES)),
256
+ optionalJson('workflow_effects_target_json_valid', table.target),
257
+ optionalJson('workflow_effects_inputs_json_valid', table.inputs),
258
+ optionalJson('workflow_effects_baseline_json_valid', table.baseline),
259
+ optionalJson('workflow_effects_cursor_json_valid', table.cursor),
260
+ optionalJson('workflow_effects_receipt_json_valid', table.receipt),
261
+ check('workflow_effects_may_execute_boolean', sql `${table.mayExecute} IN (0, 1)`),
262
+ check('workflow_effects_attempt_count_nonnegative', sql `${table.attemptCount} >= 0`),
263
+ unique('workflow_effects_run_action_unique').on(table.runId, table.actionId),
264
+ index('workflow_effects_runnable').on(table.state, table.updatedAt).where(sql `${table.state} = 'prepared'`),
265
+ index('workflow_effects_run_state').on(table.runId, table.state)
266
+ ]);
267
+ export const workflowEffectAttempts = sqliteTable('workflow_effect_attempts', {
268
+ id: text().primaryKey(),
269
+ effectId: text('effect_id')
270
+ .notNull()
271
+ .references(() => workflowEffects.id, { onDelete: 'cascade' }),
272
+ attemptNumber: integer('attempt_number').notNull(),
273
+ state: text({ enum: WORKFLOW_EFFECT_STATES }).notNull(),
274
+ ownerInstanceId: text('owner_instance_id').notNull(),
275
+ ownerPid: integer('owner_pid').notNull(),
276
+ ownerProcessStartedAt: text('owner_process_started_at').notNull(),
277
+ ownerProtocolVersion: integer('owner_protocol_version').notNull(),
278
+ launchNonce: text('launch_nonce'),
279
+ ...externalProcessColumns(),
280
+ mayExecute: integer('may_execute', { mode: 'boolean' }).notNull().default(false),
281
+ receipt: optionalJsonColumn('receipt_json'),
282
+ evidence: optionalJsonColumn('evidence_json'),
283
+ errorCode: text('error_code'),
284
+ errorMessage: text('error_message'),
285
+ ...timestamps()
286
+ }, table => [
287
+ check('workflow_effect_attempts_attempt_positive', sql `${table.attemptNumber} > 0`),
288
+ check('workflow_effect_attempts_state_valid', enumCheck(table.state, WORKFLOW_EFFECT_STATES)),
289
+ check('workflow_effect_attempts_may_execute_boolean', sql `${table.mayExecute} IN (0, 1)`),
290
+ optionalJson('workflow_effect_attempts_receipt_json_valid', table.receipt),
291
+ optionalJson('workflow_effect_attempts_evidence_json_valid', table.evidence),
292
+ unique('workflow_effect_attempts_effect_number_unique').on(table.effectId, table.attemptNumber)
293
+ ]);
294
+ export const workflowEvents = sqliteTable('workflow_events', {
295
+ id: integer().primaryKey({ autoIncrement: true }),
296
+ runId: text('run_id')
297
+ .notNull()
298
+ .references(() => workflowRuns.id, { onDelete: 'cascade' }),
299
+ eventKey: text('event_key').notNull(),
300
+ type: text().notNull(),
301
+ data: optionalJsonColumn('data_json'),
302
+ createdAt: integer('created_at').notNull()
303
+ }, table => [
304
+ optionalJson('workflow_events_data_json_valid', table.data),
305
+ unique('workflow_events_run_event_key_unique').on(table.runId, table.eventKey),
306
+ index('workflow_events_run').on(table.runId, table.id)
307
+ ]);
308
+ export const workflowIdempotency = sqliteTable('workflow_idempotency', {
309
+ operation: text().notNull(),
310
+ clientId: text('client_id').notNull(),
311
+ requestHash: text('request_hash').notNull(),
312
+ result: jsonColumn('result_json').notNull(),
313
+ runId: text('run_id').references(() => workflowRuns.id, { onDelete: 'set null' }),
314
+ actionId: text('action_id'),
315
+ createdAt: integer('created_at').notNull()
316
+ }, table => [
317
+ primaryKey({ columns: [table.operation, table.clientId] }),
318
+ check('workflow_idempotency_request_hash_length', sql `length(${table.requestHash}) = 64`),
319
+ requiredJson('workflow_idempotency_result_json_valid', table.result)
320
+ ]);
321
+ export const relayInstances = sqliteTable('relay_instances', {
322
+ instanceId: text('instance_id').primaryKey(),
323
+ pid: integer().notNull(),
324
+ processStartedAt: text('process_started_at').notNull(),
325
+ protocolVersion: integer('protocol_version').notNull(),
326
+ canDriveUi: integer('can_drive_ui', { mode: 'boolean' }).notNull().default(true),
327
+ heartbeatAt: integer('heartbeat_at').notNull(),
328
+ registeredAt: integer('registered_at').notNull(),
329
+ metadata: optionalJsonColumn('metadata_json')
330
+ }, table => [
331
+ check('relay_instances_can_drive_ui_boolean', sql `${table.canDriveUi} IN (0, 1)`),
332
+ optionalJson('relay_instances_metadata_json_valid', table.metadata),
333
+ index('relay_instances_protocol').on(table.canDriveUi, table.protocolVersion, table.heartbeatAt)
334
+ ]);
335
+ export const uiMutex = sqliteTable('ui_mutex', {
336
+ id: integer().primaryKey(),
337
+ ...relayOwnerColumns(),
338
+ nonce: text(),
339
+ actionId: text('action_id'),
340
+ effectId: text('effect_id'),
341
+ ...externalProcessColumns(),
342
+ mayExecute: integer('may_execute', { mode: 'boolean' }).notNull().default(false),
343
+ deadlineAt: integer('deadline_at'),
344
+ acquiredAt: integer('acquired_at'),
345
+ updatedAt: integer('updated_at')
346
+ }, table => [
347
+ check('ui_mutex_singleton', sql `${table.id} = 1`),
348
+ check('ui_mutex_may_execute_boolean', sql `${table.mayExecute} IN (0, 1)`)
349
+ ]);
350
+ export const uiQuarantine = sqliteTable('ui_quarantine', {
351
+ id: integer().primaryKey(),
352
+ active: integer({ mode: 'boolean' }).notNull().default(false),
353
+ actionId: text('action_id'),
354
+ effectId: text('effect_id'),
355
+ reason: text(),
356
+ ...relayOwnerColumns(),
357
+ ...externalProcessColumns(),
358
+ createdAt: integer('created_at'),
359
+ clearedAt: integer('cleared_at'),
360
+ clearedBy: text('cleared_by')
361
+ }, table => [
362
+ check('ui_quarantine_singleton', sql `${table.id} = 1`),
363
+ check('ui_quarantine_active_boolean', sql `${table.active} IN (0, 1)`)
364
+ ]);
365
+ export const orchestrationSchema = {
366
+ orchestrationMeta,
367
+ workflowRuns,
368
+ workflowCapabilities,
369
+ workflowJobs,
370
+ workflowJobAttempts,
371
+ workflowEffects,
372
+ workflowEffectAttempts,
373
+ workflowEvents,
374
+ workflowIdempotency,
375
+ relayInstances,
376
+ uiMutex,
377
+ uiQuarantine
378
+ };
379
+ export const ORCHESTRATION_TABLE_NAMES = Object.values(orchestrationSchema).map(getTableName);
380
+ const optionalStoredJsonSchema = (value) => z.object({ value }).nullable();
381
+ const optionalUnknownJsonSchema = optionalStoredJsonSchema(jsonSchema);
382
+ export const workflowRunSelectSchema = createSelectSchema(workflowRuns, {
383
+ target: workflowTargetSchema,
384
+ roles: frozenWorkflowRolesSchema,
385
+ pristineEvidence: optionalUnknownJsonSchema,
386
+ deliveryBaseline: optionalUnknownJsonSchema,
387
+ blockedCandidates: optionalStoredJsonSchema(z.array(workflowAdoptionCandidateSchema))
388
+ });
389
+ export const workflowCapabilitySelectSchema = createSelectSchema(workflowCapabilities, {
390
+ allowedRoles: z.array(z.enum(WORKFLOW_JOB_ROLES))
391
+ });
392
+ export const workflowJobSelectSchema = createSelectSchema(workflowJobs, {
393
+ resolvedRole: resolvedWorkflowRoleSchema,
394
+ transcriptCursor: optionalUnknownJsonSchema,
395
+ outcome: optionalUnknownJsonSchema,
396
+ taskReceipt: optionalUnknownJsonSchema,
397
+ batonReceipt: optionalUnknownJsonSchema
398
+ });
399
+ export const workflowJobAttemptSelectSchema = createSelectSchema(workflowJobAttempts, {
400
+ outcome: optionalUnknownJsonSchema,
401
+ failureEvidence: optionalUnknownJsonSchema
402
+ });
403
+ export const workflowEffectSelectSchema = createSelectSchema(workflowEffects, {
404
+ target: optionalUnknownJsonSchema,
405
+ inputs: optionalUnknownJsonSchema,
406
+ baseline: optionalUnknownJsonSchema,
407
+ cursor: optionalUnknownJsonSchema,
408
+ receipt: optionalUnknownJsonSchema
409
+ });
410
+ export const workflowEffectAttemptSelectSchema = createSelectSchema(workflowEffectAttempts, {
411
+ receipt: optionalUnknownJsonSchema,
412
+ evidence: optionalUnknownJsonSchema
413
+ });
414
+ export const workflowEventSelectSchema = createSelectSchema(workflowEvents, { data: optionalUnknownJsonSchema });
415
+ export const relayInstanceSelectSchema = createSelectSchema(relayInstances, { metadata: optionalUnknownJsonSchema });
416
+ export const uiMutexSelectSchema = createSelectSchema(uiMutex);
417
+ export const uiQuarantineSelectSchema = createSelectSchema(uiQuarantine);
@@ -59,7 +59,7 @@ function fields(text) {
59
59
  /**
60
60
  * Split an inline TOML table on top-level commas. Preview URL fields are strings,
61
61
  * so this deliberately implements only the small grammar the Conductor schema
62
- * permits instead of pulling a TOML runtime into the dependency-free relay.
62
+ * permits instead of pulling a TOML runtime into the lightweight relay.
63
63
  */
64
64
  function inlineFields(text) {
65
65
  const parts = [];
@@ -587,17 +587,142 @@ export class Reads {
587
587
  }
588
588
  /** Has this send created either a durable user row or a newly-owned outbox item? */
589
589
  promptDeliveredSince(sessionId, text, before) {
590
+ return this.deliveryReceiptSince(sessionId, text, before) !== null;
591
+ }
592
+ deliveryMessageRows(where, params) {
593
+ const select = (turn) => `SELECT rowid, id, content, ${turn} AS turn_id
594
+ FROM session_messages
595
+ WHERE session_id = ? AND role = 'user' AND ${where}
596
+ ORDER BY rowid ASC`;
597
+ try {
598
+ return this.db.query(select('turn_id'), params);
599
+ }
600
+ catch (error) {
601
+ // turn_id predates the current outbox protocol. Preserve the tagged receipt
602
+ // on older Conductor builds, with a truthful null rather than inventing a turn.
603
+ if (!(error instanceof Error && /no such column:\s*turn_id/i.test(error.message)))
604
+ throw error;
605
+ return this.db.query(select('NULL'), params);
606
+ }
607
+ }
608
+ rawOutboxRows(sessionId, messageId) {
609
+ if (!this.hasMessageOutbox())
610
+ return [];
611
+ try {
612
+ return this.db.query(`SELECT message_id, delivery_payload
613
+ FROM session_messages_outbox
614
+ WHERE session_id = ?${messageId === undefined ? '' : ' AND message_id = ?'}
615
+ ORDER BY COALESCE(queue_order, 2147483647), created_at ASC`, messageId === undefined ? [sessionId] : [sessionId, messageId]);
616
+ }
617
+ catch (error) {
618
+ if (!(error instanceof Error && /no such table:\s*session_messages_outbox/i.test(error.message)))
619
+ throw error;
620
+ this.messageOutboxAvailable = false;
621
+ this.messageOutboxCheckedAt = Date.now();
622
+ return [];
623
+ }
624
+ }
625
+ outboxText(payload) {
626
+ try {
627
+ const message = JSON.parse(payload ?? '').message;
628
+ return typeof message === 'string' ? message : null;
629
+ }
630
+ catch {
631
+ return null;
632
+ }
633
+ }
634
+ /**
635
+ * Find the first positive receipt created after a pre-send cursor. Matching uses
636
+ * the private raw text inside this read-only method: parsed transcripts intentionally
637
+ * redact Workflow capabilities, which would make two rotated envelopes look equal.
638
+ * A durable row wins over a stale outbox snapshot if promotion straddles the reads.
639
+ */
640
+ deliveryReceiptSince(sessionId, text, before) {
590
641
  const target = text.trim();
591
- const durable = this.durableMessages(sessionId, before.rowid).entries;
592
- const pending = this.outboxMessages(sessionId, false);
593
- return [...durable, ...pending].some(entry => entry.role === 'user' && entry.text.trim() === target && !before.outboxIds.has(entry.id));
642
+ const durableReceipt = () => {
643
+ const durable = this.deliveryMessageRows('rowid > ?', [sessionId, before.rowid]).find(row => row.content?.trim() === target && !before.outboxIds.has(row.id));
644
+ return durable ? { kind: 'message', id: durable.id, rowid: durable.rowid, turnId: durable.turn_id ?? null } : null;
645
+ };
646
+ const durable = durableReceipt();
647
+ if (durable)
648
+ return durable;
649
+ const accepted = this.rawOutboxRows(sessionId).find(row => this.outboxText(row.delivery_payload)?.trim() === target && !before.outboxIds.has(row.message_id));
650
+ if (accepted)
651
+ return { kind: 'outbox', id: accepted.message_id };
652
+ // Promotion deletes the outbox row and inserts the durable row. If that
653
+ // transaction lands after our first query but before the outbox query, one
654
+ // final durable read closes the otherwise false-negative snapshot gap.
655
+ return durableReceipt();
656
+ }
657
+ /**
658
+ * Recover an orphaned Workflow send by its persisted non-secret correlation
659
+ * marker. Raw message text stays inside this read-only boundary; callers receive
660
+ * only the tagged receipt, and ordinary transcript surfaces still scrub the whole
661
+ * private envelope that contains the marker.
662
+ */
663
+ deliveryReceiptContainingSince(sessionId, marker, before) {
664
+ if (!marker)
665
+ return null;
666
+ const durableReceipt = () => {
667
+ const durable = this.deliveryMessageRows('rowid > ?', [sessionId, before.rowid]).find(row => row.content?.includes(marker) && !before.outboxIds.has(row.id));
668
+ return durable ? { kind: 'message', id: durable.id, rowid: durable.rowid, turnId: durable.turn_id ?? null } : null;
669
+ };
670
+ const durable = durableReceipt();
671
+ if (durable)
672
+ return durable;
673
+ const accepted = this.rawOutboxRows(sessionId).find(row => this.outboxText(row.delivery_payload)?.includes(marker) && !before.outboxIds.has(row.message_id));
674
+ return accepted ? { kind: 'outbox', id: accepted.message_id } : durableReceipt();
675
+ }
676
+ /** Follow one accepted id as Conductor promotes it from outbox to transcript. */
677
+ deliveryReceiptForId(sessionId, messageId) {
678
+ if (!this.hasMessageOutbox()) {
679
+ const durable = this.deliveryMessageRows('id = ?', [sessionId, messageId])[0];
680
+ return durable ? { kind: 'message', id: durable.id, rowid: durable.rowid, turnId: durable.turn_id ?? null } : null;
681
+ }
682
+ const select = (turn) => `
683
+ SELECT kind, id, rowid, turn_id
684
+ FROM (
685
+ SELECT 'message' AS kind, id, rowid, ${turn} AS turn_id, 0 AS preference
686
+ FROM session_messages
687
+ WHERE session_id = ? AND role = 'user' AND id = ?
688
+ UNION ALL
689
+ SELECT 'outbox' AS kind, message_id AS id, NULL AS rowid, NULL AS turn_id, 1 AS preference
690
+ FROM session_messages_outbox
691
+ WHERE session_id = ? AND message_id = ?
692
+ )
693
+ ORDER BY preference
694
+ LIMIT 1`;
695
+ let row;
696
+ try {
697
+ row = this.db.query(select('turn_id'), [sessionId, messageId, sessionId, messageId])[0];
698
+ }
699
+ catch (error) {
700
+ if (error instanceof Error && /no such column:\s*turn_id/i.test(error.message)) {
701
+ row = this.db.query(select('NULL'), [sessionId, messageId, sessionId, messageId])[0];
702
+ }
703
+ else if (error instanceof Error && /no such table:\s*session_messages_outbox/i.test(error.message)) {
704
+ this.messageOutboxAvailable = false;
705
+ this.messageOutboxCheckedAt = Date.now();
706
+ return this.deliveryReceiptForId(sessionId, messageId);
707
+ }
708
+ else {
709
+ throw error;
710
+ }
711
+ }
712
+ if (!row)
713
+ return null;
714
+ if (row.kind === 'outbox')
715
+ return { kind: 'outbox', id: row.id };
716
+ if (!Number.isSafeInteger(row.rowid))
717
+ return null;
718
+ return { kind: 'message', id: row.id, rowid: row.rowid, turnId: row.turn_id ?? null };
594
719
  }
595
720
  /** Incremental durable transcript fetch, without the independently replaced outbox snapshot. */
596
- durableMessages(sessionId, afterRowid) {
721
+ durableMessages(sessionId, afterRowid, turnId) {
597
722
  const rows = this.db.query(`SELECT rowid, id, role, content, full_message, created_at, sent_at, queue_order
598
723
  FROM session_messages
599
- WHERE session_id = ? AND rowid > ?
600
- ORDER BY rowid ASC`, [sessionId, afterRowid]);
724
+ WHERE session_id = ? AND rowid > ?${turnId === undefined ? '' : ' AND turn_id = ?'}
725
+ ORDER BY rowid ASC`, turnId === undefined ? [sessionId, afterRowid] : [sessionId, afterRowid, turnId]);
601
726
  const worktree = this.sessionWorktree(sessionId);
602
727
  const entries = [];
603
728
  let cursor = afterRowid;
@@ -649,4 +774,8 @@ export class Reads {
649
774
  forkTokens
650
775
  };
651
776
  }
777
+ /** Exact durable frames from one Conductor turn; used to keep managed child outcomes correlated. */
778
+ getMessagesForTurn(sessionId, turnId, afterRowid = 0) {
779
+ return this.durableMessages(sessionId, afterRowid, turnId);
780
+ }
652
781
  }