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.
- package/README.md +65 -44
- package/dist/assets/{PierrePatch-CEuLSHL_.js → PierrePatch-DF-STc4J.js} +1 -1
- package/dist/assets/index-DM7eCDXg.css +1 -0
- package/dist/assets/index-DWbWwEOp.js +59 -0
- package/dist/index.html +2 -2
- package/dist/licenses/vscode-seti.txt +48 -0
- package/dist/sw.js +1 -1
- package/dist-node/src/logbuf.js +4 -3
- package/dist-node/src/mcp-tools.js +48 -138
- package/dist-node/src/model-cache.js +22 -7
- package/dist-node/src/orchestration-db.js +2150 -0
- package/dist-node/src/orchestration-schema.generated.js +306 -0
- package/dist-node/src/orchestration-schema.js +417 -0
- package/dist-node/src/preview-urls.js +1 -1
- package/dist-node/src/reads.js +135 -6
- package/dist-node/src/relay-processes.js +230 -0
- package/dist-node/src/roles.js +64 -12
- package/dist-node/src/routes.js +14 -1
- package/dist-node/src/search-worker.js +3 -1
- package/dist-node/src/server.js +1050 -211
- package/dist-node/src/shared.js +79 -2
- package/dist-node/src/transcript.js +20 -10
- package/dist-node/src/ui-lease-watchdog.js +94 -0
- package/dist-node/src/workflow-coordinator.js +2119 -0
- package/dist-node/src/workflow-effect-runner.js +336 -0
- package/dist-node/src/workflow-http.js +141 -0
- package/dist-node/src/workflow-machine.js +112 -0
- package/dist-node/src/workflow.js +111 -22
- package/dist-node/src/writes.js +157 -12
- package/package.json +9 -2
- package/dist/assets/index-BfTQtpd5.css +0 -1
- package/dist/assets/index-BiWQQKon.js +0 -58
|
@@ -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
|
|
62
|
+
* permits instead of pulling a TOML runtime into the lightweight relay.
|
|
63
63
|
*/
|
|
64
64
|
function inlineFields(text) {
|
|
65
65
|
const parts = [];
|
package/dist-node/src/reads.js
CHANGED
|
@@ -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
|
|
592
|
-
|
|
593
|
-
|
|
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
|
}
|