@opengeni/db 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-O3D7DABC.js → chunk-B22X3IEZ.js} +369 -49
- package/dist/chunk-B22X3IEZ.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15133 -13199
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +475 -211
- package/dist/{schema-Dp2MbBxx.d.ts → schema-BN5mB9xZ.d.ts} +1365 -256
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +13 -3
- package/drizzle/0063_session_control_mega_foundation.sql +1324 -0
- package/package.json +3 -7
- package/src/index.ts +1474 -2898
- package/src/schema.ts +391 -60
- package/src/session-control.ts +1759 -0
- package/src/session-queue-commands.ts +1753 -0
- package/src/session-tool-call-settlement.ts +269 -0
- package/dist/chunk-O3D7DABC.js.map +0 -1
- package/src/session-control-cutover-audit.ts +0 -1203
|
@@ -0,0 +1,1753 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mergeResourceRefs,
|
|
3
|
+
mergeToolRefs,
|
|
4
|
+
type ReasoningEffort,
|
|
5
|
+
type ResourceRef,
|
|
6
|
+
type ToolRef,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
9
|
+
import type { Database } from "./index";
|
|
10
|
+
import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
|
|
11
|
+
import {
|
|
12
|
+
assertAgentCommandAuthorityInTransaction,
|
|
13
|
+
autoResumeSessionBranchInTransaction,
|
|
14
|
+
canonicalSessionCommandHash,
|
|
15
|
+
evaluateSessionControl,
|
|
16
|
+
lockWorkspaceInferenceControl,
|
|
17
|
+
registerInternalUpdateWakeInTransaction,
|
|
18
|
+
reserveSessionCommandReceipt,
|
|
19
|
+
registerSessionWorkflowWakeInTransaction,
|
|
20
|
+
type SessionCommandActor,
|
|
21
|
+
type SessionCommandReceiptRow,
|
|
22
|
+
SessionControlConflictError,
|
|
23
|
+
SessionControlInvariantError,
|
|
24
|
+
updateSessionCommandReceiptResult,
|
|
25
|
+
} from "./session-control";
|
|
26
|
+
import * as schema from "./schema";
|
|
27
|
+
|
|
28
|
+
export type QueueCommandConflictCode =
|
|
29
|
+
| "QUEUE_VERSION_CHANGED"
|
|
30
|
+
| "QUEUE_PROMPT_STARTED"
|
|
31
|
+
| "QUEUE_ANCHOR_CHANGED"
|
|
32
|
+
| "PROMPT_CHANGED"
|
|
33
|
+
| "DRAFT_CHANGED"
|
|
34
|
+
| "DRAFT_NOT_EMPTY";
|
|
35
|
+
|
|
36
|
+
export class QueueCommandConflictError extends Error {
|
|
37
|
+
readonly name = "QueueCommandConflictError";
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
readonly code: QueueCommandConflictCode,
|
|
41
|
+
message: string,
|
|
42
|
+
readonly current: {
|
|
43
|
+
queueVersion: number;
|
|
44
|
+
turnVersion?: number;
|
|
45
|
+
draftRevision?: number;
|
|
46
|
+
},
|
|
47
|
+
) {
|
|
48
|
+
super(message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ComposerDraftRow = typeof schema.composerDrafts.$inferSelect;
|
|
53
|
+
export type QueuedTurnRow = typeof schema.sessionTurns.$inferSelect;
|
|
54
|
+
|
|
55
|
+
export type QueueCommandResult = {
|
|
56
|
+
receipt: SessionCommandReceiptRow;
|
|
57
|
+
queueVersion: number;
|
|
58
|
+
items: QueuedTurnRow[];
|
|
59
|
+
eventIds: string[];
|
|
60
|
+
replay: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type EditQueueCommandResult = QueueCommandResult & {
|
|
64
|
+
draft: ComposerDraftRow;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type SteerQueueCommandResult = QueueCommandResult & {
|
|
68
|
+
interruptionCount: number;
|
|
69
|
+
workspaceControlEventId: string | null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type SubmitHumanPromptResult = {
|
|
73
|
+
receipt: SessionCommandReceiptRow;
|
|
74
|
+
queueVersion: number;
|
|
75
|
+
acceptedEventId: string;
|
|
76
|
+
eventIds: string[];
|
|
77
|
+
turnId: string;
|
|
78
|
+
wakeRevision: number;
|
|
79
|
+
interruptionCount: number;
|
|
80
|
+
workspaceControlEventId: string | null;
|
|
81
|
+
replay: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type AgentInternalUpdateCommandResult = {
|
|
85
|
+
receipt: SessionCommandReceiptRow;
|
|
86
|
+
updateId: string;
|
|
87
|
+
eventIds: string[];
|
|
88
|
+
wakeRevision: number | null;
|
|
89
|
+
shouldSignal: boolean;
|
|
90
|
+
workflowId: string;
|
|
91
|
+
effectiveState: "active" | "paused";
|
|
92
|
+
interruptionCount: number;
|
|
93
|
+
workspaceControlEventId: string | null;
|
|
94
|
+
replay: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
async function lockSession(
|
|
98
|
+
db: Database,
|
|
99
|
+
workspaceId: string,
|
|
100
|
+
sessionId: string,
|
|
101
|
+
): Promise<typeof schema.sessions.$inferSelect> {
|
|
102
|
+
const [session] = await db
|
|
103
|
+
.select()
|
|
104
|
+
.from(schema.sessions)
|
|
105
|
+
.where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
|
|
106
|
+
.for("update")
|
|
107
|
+
.limit(1);
|
|
108
|
+
if (!session) throw new Error(`Session not found: ${sessionId}`);
|
|
109
|
+
return session;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type SteerSupersessionResult = {
|
|
113
|
+
interruptionCount: number;
|
|
114
|
+
replacedTurn: typeof schema.sessionTurns.$inferSelect | null;
|
|
115
|
+
liveCurrentTurnId: string | null;
|
|
116
|
+
lastSequence: number;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One canonical replacement transition shared by human row/new-prompt Steer
|
|
121
|
+
* and Agent Steer. A live owner is interrupted and remains current until exact
|
|
122
|
+
* settlement; an ownerless approval/recovery/capacity turn is superseded now.
|
|
123
|
+
*/
|
|
124
|
+
export async function supersedeSessionCurrentDirectionInTransaction(
|
|
125
|
+
db: Database,
|
|
126
|
+
input: {
|
|
127
|
+
accountId: string;
|
|
128
|
+
workspaceId: string;
|
|
129
|
+
sessionId: string;
|
|
130
|
+
activeTurnId: string | null;
|
|
131
|
+
actor: SessionCommandActor;
|
|
132
|
+
operationId: string;
|
|
133
|
+
controlRevision: number;
|
|
134
|
+
lastSequence: number;
|
|
135
|
+
},
|
|
136
|
+
): Promise<SteerSupersessionResult> {
|
|
137
|
+
if (!input.activeTurnId) {
|
|
138
|
+
return {
|
|
139
|
+
interruptionCount: 0,
|
|
140
|
+
replacedTurn: null,
|
|
141
|
+
liveCurrentTurnId: null,
|
|
142
|
+
lastSequence: input.lastSequence,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const [current] = await db
|
|
146
|
+
.select()
|
|
147
|
+
.from(schema.sessionTurns)
|
|
148
|
+
.where(
|
|
149
|
+
and(
|
|
150
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
151
|
+
eq(schema.sessionTurns.sessionId, input.sessionId),
|
|
152
|
+
eq(schema.sessionTurns.id, input.activeTurnId),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
.for("update")
|
|
156
|
+
.limit(1);
|
|
157
|
+
if (!current) {
|
|
158
|
+
throw new SessionControlInvariantError(
|
|
159
|
+
`Session ${input.sessionId} points to missing active turn ${input.activeTurnId}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!["running", "requires_action", "recovering", "waiting_capacity"].includes(current.status)) {
|
|
163
|
+
throw new SessionControlInvariantError(
|
|
164
|
+
`Active turn ${current.id} cannot be Steered from ${current.status}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (current.status === "running" && !current.activeAttemptId) {
|
|
168
|
+
throw new SessionControlInvariantError(
|
|
169
|
+
`Running turn ${current.id} has no first-class attempt owner`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (current.activeAttemptId) {
|
|
173
|
+
if (current.status !== "running") {
|
|
174
|
+
throw new SessionControlInvariantError(
|
|
175
|
+
`Live attempt ${current.activeAttemptId} owns non-running turn ${current.id}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const [interruption] = await db
|
|
179
|
+
.insert(schema.sessionAttemptInterruptions)
|
|
180
|
+
.values({
|
|
181
|
+
accountId: input.accountId,
|
|
182
|
+
workspaceId: input.workspaceId,
|
|
183
|
+
sessionId: input.sessionId,
|
|
184
|
+
operationId: input.operationId,
|
|
185
|
+
attemptId: current.activeAttemptId,
|
|
186
|
+
kind: "steer",
|
|
187
|
+
controlRevision: input.controlRevision,
|
|
188
|
+
})
|
|
189
|
+
.onConflictDoNothing()
|
|
190
|
+
.returning({ id: schema.sessionAttemptInterruptions.id });
|
|
191
|
+
return {
|
|
192
|
+
interruptionCount: interruption ? 1 : 0,
|
|
193
|
+
replacedTurn: current,
|
|
194
|
+
liveCurrentTurnId: current.id,
|
|
195
|
+
lastSequence: input.lastSequence,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const now = new Date();
|
|
200
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(db, {
|
|
201
|
+
accountId: input.accountId,
|
|
202
|
+
workspaceId: input.workspaceId,
|
|
203
|
+
sessionId: input.sessionId,
|
|
204
|
+
turnId: current.id,
|
|
205
|
+
reason: "steer",
|
|
206
|
+
sequence: input.lastSequence,
|
|
207
|
+
now,
|
|
208
|
+
});
|
|
209
|
+
await db
|
|
210
|
+
.update(schema.sessionTurns)
|
|
211
|
+
.set({
|
|
212
|
+
status: "superseded",
|
|
213
|
+
cancelledBy:
|
|
214
|
+
input.actor.type === "agent_attempt"
|
|
215
|
+
? `attempt:${input.actor.attemptId}`
|
|
216
|
+
: input.actor.subjectId,
|
|
217
|
+
cancelReason: "steer",
|
|
218
|
+
version: current.version + 1,
|
|
219
|
+
finishedAt: now,
|
|
220
|
+
updatedAt: now,
|
|
221
|
+
})
|
|
222
|
+
.where(eq(schema.sessionTurns.id, current.id));
|
|
223
|
+
await db
|
|
224
|
+
.update(schema.sessionSystemUpdates)
|
|
225
|
+
.set({ state: "pending", deliveredTurnId: null, deliveredAt: null })
|
|
226
|
+
.where(
|
|
227
|
+
and(
|
|
228
|
+
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
229
|
+
eq(schema.sessionSystemUpdates.sessionId, input.sessionId),
|
|
230
|
+
eq(schema.sessionSystemUpdates.deliveredTurnId, current.id),
|
|
231
|
+
eq(schema.sessionSystemUpdates.state, "delivered"),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
if (current.status === "waiting_capacity") {
|
|
235
|
+
await db
|
|
236
|
+
.update(schema.codexCapacityWaiters)
|
|
237
|
+
.set({ status: "superseded", updatedAt: now })
|
|
238
|
+
.where(
|
|
239
|
+
and(
|
|
240
|
+
eq(schema.codexCapacityWaiters.workspaceId, input.workspaceId),
|
|
241
|
+
eq(schema.codexCapacityWaiters.sessionId, input.sessionId),
|
|
242
|
+
eq(schema.codexCapacityWaiters.blockedTurnId, current.id),
|
|
243
|
+
eq(schema.codexCapacityWaiters.status, "waiting"),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
interruptionCount: 0,
|
|
249
|
+
replacedTurn: current,
|
|
250
|
+
liveCurrentTurnId: null,
|
|
251
|
+
lastSequence: closedTools.sequence,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function loadQueuedTurns(
|
|
256
|
+
db: Database,
|
|
257
|
+
workspaceId: string,
|
|
258
|
+
sessionId: string,
|
|
259
|
+
lock = false,
|
|
260
|
+
): Promise<QueuedTurnRow[]> {
|
|
261
|
+
const query = db
|
|
262
|
+
.select()
|
|
263
|
+
.from(schema.sessionTurns)
|
|
264
|
+
.where(
|
|
265
|
+
and(
|
|
266
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
267
|
+
eq(schema.sessionTurns.sessionId, sessionId),
|
|
268
|
+
eq(schema.sessionTurns.status, "queued"),
|
|
269
|
+
inArray(schema.sessionTurns.source, ["user", "api"]),
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
.orderBy(
|
|
273
|
+
asc(schema.sessionTurns.position),
|
|
274
|
+
asc(schema.sessionTurns.createdAt),
|
|
275
|
+
asc(schema.sessionTurns.id),
|
|
276
|
+
);
|
|
277
|
+
return lock ? await query.for("update") : await query;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function normalizeQueuePositions(
|
|
281
|
+
db: Database,
|
|
282
|
+
workspaceId: string,
|
|
283
|
+
sessionId: string,
|
|
284
|
+
orderedIds: string[],
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
if (orderedIds.length > 0) {
|
|
287
|
+
const orderedValues = sql.join(
|
|
288
|
+
orderedIds.map((id, index) => sql`(${id}::uuid, ${index + 1}::bigint)`),
|
|
289
|
+
sql`, `,
|
|
290
|
+
);
|
|
291
|
+
await db.execute(sql`
|
|
292
|
+
with ordered(id, position) as (values ${orderedValues})
|
|
293
|
+
update ${schema.sessionTurns} turn
|
|
294
|
+
set position = ordered.position, updated_at = now()
|
|
295
|
+
from ordered
|
|
296
|
+
where turn.workspace_id = ${workspaceId}
|
|
297
|
+
and turn.session_id = ${sessionId}
|
|
298
|
+
and turn.id = ordered.id
|
|
299
|
+
and turn.status = 'queued'
|
|
300
|
+
`);
|
|
301
|
+
}
|
|
302
|
+
await db
|
|
303
|
+
.update(schema.sessions)
|
|
304
|
+
.set({
|
|
305
|
+
queueHeadPosition: 0,
|
|
306
|
+
queueTailPosition: orderedIds.length,
|
|
307
|
+
updatedAt: new Date(),
|
|
308
|
+
})
|
|
309
|
+
.where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function draftIsNonEmpty(draft: ComposerDraftRow): boolean {
|
|
313
|
+
return (
|
|
314
|
+
draft.text.length > 0 ||
|
|
315
|
+
draft.resources.length > 0 ||
|
|
316
|
+
draft.tools.length > 0 ||
|
|
317
|
+
draft.sourceTurnId !== null
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function getComposerDraftInTransaction(
|
|
322
|
+
db: Database,
|
|
323
|
+
input: { workspaceId: string; sessionId: string; subjectId: string; lock?: boolean },
|
|
324
|
+
): Promise<ComposerDraftRow | null> {
|
|
325
|
+
const query = db
|
|
326
|
+
.select()
|
|
327
|
+
.from(schema.composerDrafts)
|
|
328
|
+
.where(
|
|
329
|
+
and(
|
|
330
|
+
eq(schema.composerDrafts.workspaceId, input.workspaceId),
|
|
331
|
+
eq(schema.composerDrafts.sessionId, input.sessionId),
|
|
332
|
+
eq(schema.composerDrafts.subjectId, input.subjectId),
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
.limit(1);
|
|
336
|
+
const rows = input.lock ? await query.for("update") : await query;
|
|
337
|
+
return rows[0] ?? null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function saveComposerDraftInTransaction(
|
|
341
|
+
db: Database,
|
|
342
|
+
input: {
|
|
343
|
+
accountId: string;
|
|
344
|
+
workspaceId: string;
|
|
345
|
+
sessionId: string;
|
|
346
|
+
subjectId: string;
|
|
347
|
+
expectedRevision: number;
|
|
348
|
+
text: string;
|
|
349
|
+
resources: ResourceRef[];
|
|
350
|
+
tools: ToolRef[];
|
|
351
|
+
model: string;
|
|
352
|
+
reasoningEffort: ReasoningEffort;
|
|
353
|
+
},
|
|
354
|
+
): Promise<ComposerDraftRow> {
|
|
355
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
|
|
356
|
+
await lockSession(db, input.workspaceId, input.sessionId);
|
|
357
|
+
const current = await getComposerDraftInTransaction(db, { ...input, lock: true });
|
|
358
|
+
const currentRevision = current?.revision ?? 0;
|
|
359
|
+
if (currentRevision !== input.expectedRevision) {
|
|
360
|
+
throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
|
|
361
|
+
queueVersion: 0,
|
|
362
|
+
draftRevision: currentRevision,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const revision = currentRevision + 1;
|
|
366
|
+
const values = {
|
|
367
|
+
accountId: input.accountId,
|
|
368
|
+
workspaceId: input.workspaceId,
|
|
369
|
+
sessionId: input.sessionId,
|
|
370
|
+
subjectId: input.subjectId,
|
|
371
|
+
revision,
|
|
372
|
+
text: input.text,
|
|
373
|
+
resources: input.resources,
|
|
374
|
+
tools: input.tools,
|
|
375
|
+
model: input.model,
|
|
376
|
+
reasoningEffort: input.reasoningEffort,
|
|
377
|
+
sourceTurnId: null,
|
|
378
|
+
sourceTurnVersion: null,
|
|
379
|
+
updatedAt: new Date(),
|
|
380
|
+
};
|
|
381
|
+
const [saved] = current
|
|
382
|
+
? await db
|
|
383
|
+
.update(schema.composerDrafts)
|
|
384
|
+
.set(values)
|
|
385
|
+
.where(eq(schema.composerDrafts.id, current.id))
|
|
386
|
+
.returning()
|
|
387
|
+
: await db.insert(schema.composerDrafts).values(values).returning();
|
|
388
|
+
if (!saved) throw new Error("Composer draft did not save");
|
|
389
|
+
return saved;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function moveQueuedTurnInTransaction(
|
|
393
|
+
db: Database,
|
|
394
|
+
input: {
|
|
395
|
+
accountId: string;
|
|
396
|
+
workspaceId: string;
|
|
397
|
+
sessionId: string;
|
|
398
|
+
turnId: string;
|
|
399
|
+
beforeTurnId: string | null;
|
|
400
|
+
expectedQueueVersion: number;
|
|
401
|
+
actor: SessionCommandActor;
|
|
402
|
+
operationKey: string;
|
|
403
|
+
},
|
|
404
|
+
): Promise<QueueCommandResult> {
|
|
405
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
|
|
406
|
+
const session = await lockSession(db, input.workspaceId, input.sessionId);
|
|
407
|
+
const requestHash = canonicalSessionCommandHash({
|
|
408
|
+
beforeTurnId: input.beforeTurnId,
|
|
409
|
+
expectedQueueVersion: input.expectedQueueVersion,
|
|
410
|
+
});
|
|
411
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
412
|
+
accountId: input.accountId,
|
|
413
|
+
workspaceId: input.workspaceId,
|
|
414
|
+
actor: input.actor,
|
|
415
|
+
action: "queue.move",
|
|
416
|
+
targetSessionId: input.sessionId,
|
|
417
|
+
targetTurnId: input.turnId,
|
|
418
|
+
operationKey: input.operationKey,
|
|
419
|
+
canonicalRequestHash: requestHash,
|
|
420
|
+
});
|
|
421
|
+
if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
|
|
422
|
+
return {
|
|
423
|
+
receipt: reserved.receipt,
|
|
424
|
+
queueVersion: session.queueVersion,
|
|
425
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
426
|
+
eventIds: [],
|
|
427
|
+
replay: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (session.queueVersion !== input.expectedQueueVersion) {
|
|
431
|
+
throw new QueueCommandConflictError("QUEUE_VERSION_CHANGED", "Queue order changed", {
|
|
432
|
+
queueVersion: session.queueVersion,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const rows = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
|
|
436
|
+
const target = rows.find((row) => row.id === input.turnId);
|
|
437
|
+
if (!target) {
|
|
438
|
+
throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
|
|
439
|
+
queueVersion: session.queueVersion,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (input.beforeTurnId === input.turnId) {
|
|
443
|
+
throw new QueueCommandConflictError(
|
|
444
|
+
"QUEUE_ANCHOR_CHANGED",
|
|
445
|
+
"Prompt cannot move before itself",
|
|
446
|
+
{
|
|
447
|
+
queueVersion: session.queueVersion,
|
|
448
|
+
turnVersion: target.version,
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const withoutTarget = rows.filter((row) => row.id !== input.turnId);
|
|
453
|
+
const anchorIndex =
|
|
454
|
+
input.beforeTurnId === null
|
|
455
|
+
? withoutTarget.length
|
|
456
|
+
: withoutTarget.findIndex((row) => row.id === input.beforeTurnId);
|
|
457
|
+
if (anchorIndex < 0) {
|
|
458
|
+
throw new QueueCommandConflictError("QUEUE_ANCHOR_CHANGED", "Queue anchor changed", {
|
|
459
|
+
queueVersion: session.queueVersion,
|
|
460
|
+
turnVersion: target.version,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const ordered = [...withoutTarget];
|
|
464
|
+
ordered.splice(anchorIndex, 0, target);
|
|
465
|
+
const changed = ordered.some((row, index) => row.id !== rows[index]?.id);
|
|
466
|
+
const queueVersion = changed ? session.queueVersion + 1 : session.queueVersion;
|
|
467
|
+
const eventIds: string[] = [];
|
|
468
|
+
if (changed) {
|
|
469
|
+
await normalizeQueuePositions(
|
|
470
|
+
db,
|
|
471
|
+
input.workspaceId,
|
|
472
|
+
input.sessionId,
|
|
473
|
+
ordered.map((row) => row.id),
|
|
474
|
+
);
|
|
475
|
+
const [event] = await db
|
|
476
|
+
.insert(schema.sessionEvents)
|
|
477
|
+
.values({
|
|
478
|
+
accountId: input.accountId,
|
|
479
|
+
workspaceId: input.workspaceId,
|
|
480
|
+
sessionId: input.sessionId,
|
|
481
|
+
sequence: session.lastSequence + 1,
|
|
482
|
+
type: "session.queue.changed",
|
|
483
|
+
turnId: target.id,
|
|
484
|
+
payload: {
|
|
485
|
+
operation: "move",
|
|
486
|
+
queueVersion,
|
|
487
|
+
turnId: target.id,
|
|
488
|
+
beforeTurnId: input.beforeTurnId,
|
|
489
|
+
},
|
|
490
|
+
occurredAt: new Date(),
|
|
491
|
+
})
|
|
492
|
+
.returning({ id: schema.sessionEvents.id });
|
|
493
|
+
if (!event) throw new Error("Queue move event was not inserted");
|
|
494
|
+
eventIds.push(event.id);
|
|
495
|
+
await db
|
|
496
|
+
.update(schema.sessions)
|
|
497
|
+
.set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: new Date() })
|
|
498
|
+
.where(eq(schema.sessions.id, input.sessionId));
|
|
499
|
+
}
|
|
500
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
501
|
+
queueVersion,
|
|
502
|
+
turnVersion: target.version,
|
|
503
|
+
result: { changed, beforeTurnId: input.beforeTurnId },
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
receipt,
|
|
507
|
+
queueVersion,
|
|
508
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
509
|
+
eventIds,
|
|
510
|
+
replay: false,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function deleteSessionQueueItemInTransaction(
|
|
515
|
+
db: Database,
|
|
516
|
+
input: {
|
|
517
|
+
accountId: string;
|
|
518
|
+
workspaceId: string;
|
|
519
|
+
sessionId: string;
|
|
520
|
+
turnId: string;
|
|
521
|
+
expectedTurnVersion: number;
|
|
522
|
+
actor: SessionCommandActor;
|
|
523
|
+
operationKey: string;
|
|
524
|
+
reason?: string | null;
|
|
525
|
+
},
|
|
526
|
+
): Promise<QueueCommandResult> {
|
|
527
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
|
|
528
|
+
const session = await lockSession(db, input.workspaceId, input.sessionId);
|
|
529
|
+
const requestHash = canonicalSessionCommandHash({
|
|
530
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
531
|
+
reason: input.reason ?? null,
|
|
532
|
+
});
|
|
533
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
534
|
+
accountId: input.accountId,
|
|
535
|
+
workspaceId: input.workspaceId,
|
|
536
|
+
actor: input.actor,
|
|
537
|
+
action: "queue.delete",
|
|
538
|
+
targetSessionId: input.sessionId,
|
|
539
|
+
targetTurnId: input.turnId,
|
|
540
|
+
operationKey: input.operationKey,
|
|
541
|
+
canonicalRequestHash: requestHash,
|
|
542
|
+
});
|
|
543
|
+
if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
|
|
544
|
+
return {
|
|
545
|
+
receipt: reserved.receipt,
|
|
546
|
+
queueVersion: session.queueVersion,
|
|
547
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
548
|
+
eventIds: [],
|
|
549
|
+
replay: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const [turn] = await db
|
|
553
|
+
.select()
|
|
554
|
+
.from(schema.sessionTurns)
|
|
555
|
+
.where(
|
|
556
|
+
and(
|
|
557
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
558
|
+
eq(schema.sessionTurns.sessionId, input.sessionId),
|
|
559
|
+
eq(schema.sessionTurns.id, input.turnId),
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
.for("update")
|
|
563
|
+
.limit(1);
|
|
564
|
+
if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
|
|
565
|
+
throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
|
|
566
|
+
queueVersion: session.queueVersion,
|
|
567
|
+
...(turn ? { turnVersion: turn.version } : {}),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (turn.version !== input.expectedTurnVersion) {
|
|
571
|
+
throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
|
|
572
|
+
queueVersion: session.queueVersion,
|
|
573
|
+
turnVersion: turn.version,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const now = new Date();
|
|
577
|
+
const queueVersion = session.queueVersion + 1;
|
|
578
|
+
await db
|
|
579
|
+
.update(schema.sessionTurns)
|
|
580
|
+
.set({
|
|
581
|
+
status: "cancelled",
|
|
582
|
+
cancelledBy:
|
|
583
|
+
input.actor.type === "agent_attempt"
|
|
584
|
+
? `attempt:${input.actor.attemptId}`
|
|
585
|
+
: input.actor.subjectId,
|
|
586
|
+
cancelReason: input.reason ?? "human_delete",
|
|
587
|
+
version: turn.version + 1,
|
|
588
|
+
finishedAt: now,
|
|
589
|
+
updatedAt: now,
|
|
590
|
+
})
|
|
591
|
+
.where(eq(schema.sessionTurns.id, turn.id));
|
|
592
|
+
const remaining = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
|
|
593
|
+
await normalizeQueuePositions(
|
|
594
|
+
db,
|
|
595
|
+
input.workspaceId,
|
|
596
|
+
input.sessionId,
|
|
597
|
+
remaining.map((row) => row.id),
|
|
598
|
+
);
|
|
599
|
+
const [event] = await db
|
|
600
|
+
.insert(schema.sessionEvents)
|
|
601
|
+
.values({
|
|
602
|
+
accountId: input.accountId,
|
|
603
|
+
workspaceId: input.workspaceId,
|
|
604
|
+
sessionId: input.sessionId,
|
|
605
|
+
sequence: session.lastSequence + 1,
|
|
606
|
+
type: "session.queue.changed",
|
|
607
|
+
turnId: turn.id,
|
|
608
|
+
payload: {
|
|
609
|
+
operation: "delete",
|
|
610
|
+
queueVersion,
|
|
611
|
+
turnId: turn.id,
|
|
612
|
+
},
|
|
613
|
+
occurredAt: now,
|
|
614
|
+
})
|
|
615
|
+
.returning({ id: schema.sessionEvents.id });
|
|
616
|
+
if (!event) throw new Error("Queue delete event was not inserted");
|
|
617
|
+
await db
|
|
618
|
+
.update(schema.sessions)
|
|
619
|
+
.set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
|
|
620
|
+
.where(eq(schema.sessions.id, input.sessionId));
|
|
621
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
622
|
+
queueVersion,
|
|
623
|
+
turnVersion: turn.version + 1,
|
|
624
|
+
result: { reason: input.reason ?? "human_delete" },
|
|
625
|
+
});
|
|
626
|
+
return {
|
|
627
|
+
receipt,
|
|
628
|
+
queueVersion,
|
|
629
|
+
items: remaining,
|
|
630
|
+
eventIds: [event.id],
|
|
631
|
+
replay: false,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export async function editQueuedTurnInTransaction(
|
|
636
|
+
db: Database,
|
|
637
|
+
input: {
|
|
638
|
+
accountId: string;
|
|
639
|
+
workspaceId: string;
|
|
640
|
+
sessionId: string;
|
|
641
|
+
turnId: string;
|
|
642
|
+
subjectId: string;
|
|
643
|
+
expectedTurnVersion: number;
|
|
644
|
+
expectedDraftRevision: number;
|
|
645
|
+
replaceDraft: boolean;
|
|
646
|
+
actor: SessionCommandActor;
|
|
647
|
+
operationKey: string;
|
|
648
|
+
},
|
|
649
|
+
): Promise<EditQueueCommandResult> {
|
|
650
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
|
|
651
|
+
const session = await lockSession(db, input.workspaceId, input.sessionId);
|
|
652
|
+
const requestHash = canonicalSessionCommandHash({
|
|
653
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
654
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
655
|
+
replaceDraft: input.replaceDraft,
|
|
656
|
+
});
|
|
657
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
658
|
+
accountId: input.accountId,
|
|
659
|
+
workspaceId: input.workspaceId,
|
|
660
|
+
actor: input.actor,
|
|
661
|
+
action: "queue.edit",
|
|
662
|
+
targetSessionId: input.sessionId,
|
|
663
|
+
targetTurnId: input.turnId,
|
|
664
|
+
operationKey: input.operationKey,
|
|
665
|
+
canonicalRequestHash: requestHash,
|
|
666
|
+
});
|
|
667
|
+
const existingDraft = await getComposerDraftInTransaction(db, { ...input, lock: true });
|
|
668
|
+
if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
|
|
669
|
+
if (!existingDraft) throw new Error("Replayed queue Edit has no durable draft");
|
|
670
|
+
return {
|
|
671
|
+
receipt: reserved.receipt,
|
|
672
|
+
queueVersion: session.queueVersion,
|
|
673
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
674
|
+
draft: existingDraft,
|
|
675
|
+
eventIds: [],
|
|
676
|
+
replay: true,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const draftRevision = existingDraft?.revision ?? 0;
|
|
680
|
+
if (draftRevision !== input.expectedDraftRevision) {
|
|
681
|
+
throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
|
|
682
|
+
queueVersion: session.queueVersion,
|
|
683
|
+
draftRevision,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (existingDraft && draftIsNonEmpty(existingDraft) && !input.replaceDraft) {
|
|
687
|
+
throw new QueueCommandConflictError("DRAFT_NOT_EMPTY", "Composer draft is not empty", {
|
|
688
|
+
queueVersion: session.queueVersion,
|
|
689
|
+
draftRevision,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const [turn] = await db
|
|
693
|
+
.select()
|
|
694
|
+
.from(schema.sessionTurns)
|
|
695
|
+
.where(
|
|
696
|
+
and(
|
|
697
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
698
|
+
eq(schema.sessionTurns.sessionId, input.sessionId),
|
|
699
|
+
eq(schema.sessionTurns.id, input.turnId),
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
.for("update")
|
|
703
|
+
.limit(1);
|
|
704
|
+
if (!turn || turn.status !== "queued" || !["user", "api"].includes(turn.source)) {
|
|
705
|
+
throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
|
|
706
|
+
queueVersion: session.queueVersion,
|
|
707
|
+
draftRevision,
|
|
708
|
+
...(turn ? { turnVersion: turn.version } : {}),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (turn.version !== input.expectedTurnVersion) {
|
|
712
|
+
throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
|
|
713
|
+
queueVersion: session.queueVersion,
|
|
714
|
+
turnVersion: turn.version,
|
|
715
|
+
draftRevision,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
const nextDraftRevision = draftRevision + 1;
|
|
719
|
+
const draftValues = {
|
|
720
|
+
accountId: input.accountId,
|
|
721
|
+
workspaceId: input.workspaceId,
|
|
722
|
+
sessionId: input.sessionId,
|
|
723
|
+
subjectId: input.subjectId,
|
|
724
|
+
revision: nextDraftRevision,
|
|
725
|
+
text: turn.prompt,
|
|
726
|
+
resources: turn.resources,
|
|
727
|
+
tools: turn.tools,
|
|
728
|
+
model: turn.model,
|
|
729
|
+
reasoningEffort: turn.reasoningEffort,
|
|
730
|
+
sourceTurnId: turn.id,
|
|
731
|
+
sourceTurnVersion: turn.version,
|
|
732
|
+
updatedAt: new Date(),
|
|
733
|
+
};
|
|
734
|
+
const [draft] = existingDraft
|
|
735
|
+
? await db
|
|
736
|
+
.update(schema.composerDrafts)
|
|
737
|
+
.set(draftValues)
|
|
738
|
+
.where(eq(schema.composerDrafts.id, existingDraft.id))
|
|
739
|
+
.returning()
|
|
740
|
+
: await db.insert(schema.composerDrafts).values(draftValues).returning();
|
|
741
|
+
if (!draft) throw new Error("Queue Edit did not persist its draft");
|
|
742
|
+
const now = new Date();
|
|
743
|
+
const queueVersion = session.queueVersion + 1;
|
|
744
|
+
await db
|
|
745
|
+
.update(schema.sessionTurns)
|
|
746
|
+
.set({
|
|
747
|
+
status: "withdrawn_for_edit",
|
|
748
|
+
cancelledBy: input.subjectId,
|
|
749
|
+
cancelReason: "withdrawn_for_edit",
|
|
750
|
+
version: turn.version + 1,
|
|
751
|
+
finishedAt: now,
|
|
752
|
+
updatedAt: now,
|
|
753
|
+
})
|
|
754
|
+
.where(eq(schema.sessionTurns.id, turn.id));
|
|
755
|
+
const remaining = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
|
|
756
|
+
await normalizeQueuePositions(
|
|
757
|
+
db,
|
|
758
|
+
input.workspaceId,
|
|
759
|
+
input.sessionId,
|
|
760
|
+
remaining.map((row) => row.id),
|
|
761
|
+
);
|
|
762
|
+
const [event] = await db
|
|
763
|
+
.insert(schema.sessionEvents)
|
|
764
|
+
.values({
|
|
765
|
+
accountId: input.accountId,
|
|
766
|
+
workspaceId: input.workspaceId,
|
|
767
|
+
sessionId: input.sessionId,
|
|
768
|
+
sequence: session.lastSequence + 1,
|
|
769
|
+
type: "session.queue.changed",
|
|
770
|
+
turnId: turn.id,
|
|
771
|
+
payload: {
|
|
772
|
+
operation: "edit",
|
|
773
|
+
queueVersion,
|
|
774
|
+
turnId: turn.id,
|
|
775
|
+
draftRevision: nextDraftRevision,
|
|
776
|
+
},
|
|
777
|
+
occurredAt: now,
|
|
778
|
+
})
|
|
779
|
+
.returning({ id: schema.sessionEvents.id });
|
|
780
|
+
if (!event) throw new Error("Queue edit event was not inserted");
|
|
781
|
+
await db
|
|
782
|
+
.update(schema.sessions)
|
|
783
|
+
.set({ queueVersion, lastSequence: session.lastSequence + 1, updatedAt: now })
|
|
784
|
+
.where(eq(schema.sessions.id, input.sessionId));
|
|
785
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
786
|
+
queueVersion,
|
|
787
|
+
turnVersion: turn.version + 1,
|
|
788
|
+
draftRevision: nextDraftRevision,
|
|
789
|
+
result: { sourceTurnId: turn.id },
|
|
790
|
+
});
|
|
791
|
+
return { receipt, queueVersion, items: remaining, draft, eventIds: [event.id], replay: false };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export async function steerQueuedTurnInTransaction(
|
|
795
|
+
db: Database,
|
|
796
|
+
input: {
|
|
797
|
+
accountId: string;
|
|
798
|
+
workspaceId: string;
|
|
799
|
+
sessionId: string;
|
|
800
|
+
turnId: string;
|
|
801
|
+
expectedTurnVersion: number;
|
|
802
|
+
controlEtag?: string | null;
|
|
803
|
+
actor: SessionCommandActor;
|
|
804
|
+
operationKey: string;
|
|
805
|
+
},
|
|
806
|
+
): Promise<SteerQueueCommandResult> {
|
|
807
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
808
|
+
const requestHash = canonicalSessionCommandHash({
|
|
809
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
810
|
+
controlEtag: input.controlEtag ?? null,
|
|
811
|
+
});
|
|
812
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
813
|
+
accountId: input.accountId,
|
|
814
|
+
workspaceId: input.workspaceId,
|
|
815
|
+
actor: input.actor,
|
|
816
|
+
action: "queue.steer",
|
|
817
|
+
targetSessionId: input.sessionId,
|
|
818
|
+
targetTurnId: input.turnId,
|
|
819
|
+
operationKey: input.operationKey,
|
|
820
|
+
canonicalRequestHash: requestHash,
|
|
821
|
+
});
|
|
822
|
+
if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
|
|
823
|
+
const replaySession = await lockSession(db, input.workspaceId, input.sessionId);
|
|
824
|
+
return {
|
|
825
|
+
receipt: reserved.receipt,
|
|
826
|
+
queueVersion: replaySession.queueVersion,
|
|
827
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
828
|
+
eventIds: [],
|
|
829
|
+
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
830
|
+
workspaceControlEventId:
|
|
831
|
+
typeof reserved.receipt.result.workspaceControlEventId === "string"
|
|
832
|
+
? reserved.receipt.result.workspaceControlEventId
|
|
833
|
+
: null,
|
|
834
|
+
replay: true,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (input.actor.type === "agent_attempt") {
|
|
838
|
+
await assertAgentCommandAuthorityInTransaction(db, {
|
|
839
|
+
workspaceId: input.workspaceId,
|
|
840
|
+
actor: input.actor,
|
|
841
|
+
targetSessionId: input.sessionId,
|
|
842
|
+
action: "steer",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
846
|
+
lock: "share",
|
|
847
|
+
});
|
|
848
|
+
if (input.controlEtag && input.controlEtag !== before.controlEtag) {
|
|
849
|
+
throw new SessionControlConflictError();
|
|
850
|
+
}
|
|
851
|
+
const resumed = await autoResumeSessionBranchInTransaction(db, {
|
|
852
|
+
workspaceId: input.workspaceId,
|
|
853
|
+
sessionId: input.sessionId,
|
|
854
|
+
actor:
|
|
855
|
+
input.actor.type === "agent_attempt"
|
|
856
|
+
? `attempt:${input.actor.attemptId}`
|
|
857
|
+
: input.actor.subjectId,
|
|
858
|
+
reason: "human_steer",
|
|
859
|
+
observedControlEtag: input.controlEtag ?? null,
|
|
860
|
+
});
|
|
861
|
+
const session = await lockSession(db, input.workspaceId, input.sessionId);
|
|
862
|
+
const rows = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
|
|
863
|
+
const target = rows.find((row) => row.id === input.turnId);
|
|
864
|
+
if (!target) {
|
|
865
|
+
throw new QueueCommandConflictError("QUEUE_PROMPT_STARTED", "Prompt is no longer waiting", {
|
|
866
|
+
queueVersion: session.queueVersion,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (target.version !== input.expectedTurnVersion) {
|
|
870
|
+
throw new QueueCommandConflictError("PROMPT_CHANGED", "Prompt changed", {
|
|
871
|
+
queueVersion: session.queueVersion,
|
|
872
|
+
turnVersion: target.version,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const supersession = await supersedeSessionCurrentDirectionInTransaction(db, {
|
|
877
|
+
accountId: input.accountId,
|
|
878
|
+
workspaceId: input.workspaceId,
|
|
879
|
+
sessionId: input.sessionId,
|
|
880
|
+
activeTurnId: session.activeTurnId,
|
|
881
|
+
actor: input.actor,
|
|
882
|
+
operationId: reserved.receipt.id,
|
|
883
|
+
controlRevision: resumed.revision,
|
|
884
|
+
lastSequence: session.lastSequence,
|
|
885
|
+
});
|
|
886
|
+
const interruptionCount = supersession.interruptionCount;
|
|
887
|
+
const supersededTurnId = supersession.replacedTurn?.id ?? null;
|
|
888
|
+
const liveCurrentTurnId = supersession.liveCurrentTurnId;
|
|
889
|
+
|
|
890
|
+
const withoutTarget = rows.filter((row) => row.id !== target.id);
|
|
891
|
+
const ordered = [target, ...withoutTarget];
|
|
892
|
+
await normalizeQueuePositions(
|
|
893
|
+
db,
|
|
894
|
+
input.workspaceId,
|
|
895
|
+
input.sessionId,
|
|
896
|
+
ordered.map((row) => row.id),
|
|
897
|
+
);
|
|
898
|
+
const now = new Date();
|
|
899
|
+
const queueVersion = session.queueVersion + 1;
|
|
900
|
+
await db
|
|
901
|
+
.update(schema.sessionTurns)
|
|
902
|
+
.set({ version: target.version + 1, updatedAt: now })
|
|
903
|
+
.where(eq(schema.sessionTurns.id, target.id));
|
|
904
|
+
let sequence = supersession.lastSequence;
|
|
905
|
+
const actor =
|
|
906
|
+
input.actor.type === "agent_attempt"
|
|
907
|
+
? `attempt:${input.actor.attemptId}`
|
|
908
|
+
: input.actor.subjectId;
|
|
909
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [];
|
|
910
|
+
if (supersededTurnId && !liveCurrentTurnId) {
|
|
911
|
+
eventValues.push({
|
|
912
|
+
accountId: input.accountId,
|
|
913
|
+
workspaceId: input.workspaceId,
|
|
914
|
+
sessionId: input.sessionId,
|
|
915
|
+
sequence: ++sequence,
|
|
916
|
+
type: "turn.superseded",
|
|
917
|
+
turnId: supersededTurnId,
|
|
918
|
+
payload: { reason: "steer", targetTurnId: target.id },
|
|
919
|
+
occurredAt: now,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
eventValues.push({
|
|
923
|
+
accountId: input.accountId,
|
|
924
|
+
workspaceId: input.workspaceId,
|
|
925
|
+
sessionId: input.sessionId,
|
|
926
|
+
sequence: ++sequence,
|
|
927
|
+
type: "session.control.steer_requested",
|
|
928
|
+
turnId: supersededTurnId ?? target.id,
|
|
929
|
+
payload: {
|
|
930
|
+
operationId: reserved.receipt.id,
|
|
931
|
+
targetTurnId: target.id,
|
|
932
|
+
replacedTurnId: supersededTurnId,
|
|
933
|
+
stopping: liveCurrentTurnId !== null,
|
|
934
|
+
},
|
|
935
|
+
occurredAt: now,
|
|
936
|
+
});
|
|
937
|
+
const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning({
|
|
938
|
+
id: schema.sessionEvents.id,
|
|
939
|
+
});
|
|
940
|
+
await db
|
|
941
|
+
.update(schema.sessions)
|
|
942
|
+
.set({
|
|
943
|
+
activeTurnId: liveCurrentTurnId,
|
|
944
|
+
status: liveCurrentTurnId ? session.status : "queued",
|
|
945
|
+
queueVersion,
|
|
946
|
+
queueHeadPosition: 0,
|
|
947
|
+
queueTailPosition: ordered.length,
|
|
948
|
+
lastSequence: sequence,
|
|
949
|
+
updatedAt: now,
|
|
950
|
+
})
|
|
951
|
+
.where(eq(schema.sessions.id, input.sessionId));
|
|
952
|
+
await db.insert(schema.auditEvents).values({
|
|
953
|
+
accountId: input.accountId,
|
|
954
|
+
workspaceId: input.workspaceId,
|
|
955
|
+
subjectId: actor,
|
|
956
|
+
action: "session.queue.steer",
|
|
957
|
+
targetType: "session_turn",
|
|
958
|
+
targetId: target.id,
|
|
959
|
+
metadata: {
|
|
960
|
+
operationId: reserved.receipt.id,
|
|
961
|
+
replacedTurnId: supersededTurnId,
|
|
962
|
+
interruptionCount,
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
|
|
966
|
+
accountId: input.accountId,
|
|
967
|
+
workspaceId: input.workspaceId,
|
|
968
|
+
sessionId: input.sessionId,
|
|
969
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
970
|
+
reason: "queue_steer",
|
|
971
|
+
});
|
|
972
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
973
|
+
controlRevision: resumed.revision,
|
|
974
|
+
queueVersion,
|
|
975
|
+
turnVersion: target.version + 1,
|
|
976
|
+
result: {
|
|
977
|
+
interruptionCount,
|
|
978
|
+
supersededTurnId,
|
|
979
|
+
wakeRevision,
|
|
980
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
return {
|
|
984
|
+
receipt,
|
|
985
|
+
queueVersion,
|
|
986
|
+
items: await loadQueuedTurns(db, input.workspaceId, input.sessionId),
|
|
987
|
+
eventIds: eventRows.map((event) => event.id),
|
|
988
|
+
interruptionCount,
|
|
989
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
990
|
+
replay: false,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export async function submitHumanPromptInTransaction(
|
|
995
|
+
db: Database,
|
|
996
|
+
input: {
|
|
997
|
+
accountId: string;
|
|
998
|
+
workspaceId: string;
|
|
999
|
+
sessionId: string;
|
|
1000
|
+
subjectId: string;
|
|
1001
|
+
actor: SessionCommandActor;
|
|
1002
|
+
operationKey: string;
|
|
1003
|
+
delivery: "send" | "steer";
|
|
1004
|
+
controlEtag?: string | null;
|
|
1005
|
+
expectedDraftRevision?: number | null;
|
|
1006
|
+
text: string;
|
|
1007
|
+
resources: ResourceRef[];
|
|
1008
|
+
tools: ToolRef[];
|
|
1009
|
+
model?: string | null;
|
|
1010
|
+
reasoningEffort?: ReasoningEffort | null;
|
|
1011
|
+
reasoningEffortFallback: ReasoningEffort;
|
|
1012
|
+
source: "user" | "api";
|
|
1013
|
+
mcpCredentialUpdates?: Array<{ id: string; headersEncrypted: Record<string, string> }>;
|
|
1014
|
+
},
|
|
1015
|
+
): Promise<SubmitHumanPromptResult> {
|
|
1016
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
1017
|
+
const requestHash = canonicalSessionCommandHash({
|
|
1018
|
+
delivery: input.delivery,
|
|
1019
|
+
controlEtag: input.controlEtag ?? null,
|
|
1020
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
1021
|
+
text: input.text,
|
|
1022
|
+
resources: input.resources,
|
|
1023
|
+
tools: input.tools,
|
|
1024
|
+
model: input.model ?? null,
|
|
1025
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
1026
|
+
source: input.source,
|
|
1027
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
1028
|
+
});
|
|
1029
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
1030
|
+
accountId: input.accountId,
|
|
1031
|
+
workspaceId: input.workspaceId,
|
|
1032
|
+
actor: input.actor,
|
|
1033
|
+
action: input.delivery === "steer" ? "prompt.steer" : "prompt.send",
|
|
1034
|
+
targetSessionId: input.sessionId,
|
|
1035
|
+
targetTurnId: null,
|
|
1036
|
+
operationKey: input.operationKey,
|
|
1037
|
+
canonicalRequestHash: requestHash,
|
|
1038
|
+
});
|
|
1039
|
+
if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
|
|
1040
|
+
const turnId = String(reserved.receipt.result.turnId ?? "");
|
|
1041
|
+
const acceptedEventId = String(reserved.receipt.result.acceptedEventId ?? "");
|
|
1042
|
+
const eventIds = Array.isArray(reserved.receipt.result.eventIds)
|
|
1043
|
+
? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
|
|
1044
|
+
: [];
|
|
1045
|
+
const wakeRevision = Number(reserved.receipt.result.wakeRevision ?? 0);
|
|
1046
|
+
if (!turnId || !acceptedEventId || wakeRevision < 1) {
|
|
1047
|
+
throw new SessionControlInvariantError("Replayed prompt receipt is incomplete");
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
receipt: reserved.receipt,
|
|
1051
|
+
queueVersion: Number(reserved.receipt.appliedQueueVersion),
|
|
1052
|
+
acceptedEventId,
|
|
1053
|
+
eventIds,
|
|
1054
|
+
turnId,
|
|
1055
|
+
wakeRevision,
|
|
1056
|
+
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
1057
|
+
workspaceControlEventId:
|
|
1058
|
+
typeof reserved.receipt.result.workspaceControlEventId === "string"
|
|
1059
|
+
? reserved.receipt.result.workspaceControlEventId
|
|
1060
|
+
: null,
|
|
1061
|
+
replay: true,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
1066
|
+
lock: "share",
|
|
1067
|
+
});
|
|
1068
|
+
if (input.controlEtag && input.controlEtag !== before.controlEtag) {
|
|
1069
|
+
throw new SessionControlConflictError();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const resumed = await autoResumeSessionBranchInTransaction(db, {
|
|
1073
|
+
workspaceId: input.workspaceId,
|
|
1074
|
+
sessionId: input.sessionId,
|
|
1075
|
+
actor:
|
|
1076
|
+
input.actor.type === "agent_attempt"
|
|
1077
|
+
? `attempt:${input.actor.attemptId}`
|
|
1078
|
+
: input.actor.subjectId,
|
|
1079
|
+
reason: input.delivery === "steer" ? "human_steer" : "human_send",
|
|
1080
|
+
observedControlEtag: input.controlEtag ?? null,
|
|
1081
|
+
});
|
|
1082
|
+
const session = await lockSession(db, input.workspaceId, input.sessionId);
|
|
1083
|
+
if (session.status === "cancelled") {
|
|
1084
|
+
throw new QueueCommandConflictError(
|
|
1085
|
+
"QUEUE_PROMPT_STARTED",
|
|
1086
|
+
"Cancelled session cannot accept work",
|
|
1087
|
+
{
|
|
1088
|
+
queueVersion: session.queueVersion,
|
|
1089
|
+
},
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const draft =
|
|
1094
|
+
input.expectedDraftRevision === null || input.expectedDraftRevision === undefined
|
|
1095
|
+
? null
|
|
1096
|
+
: await getComposerDraftInTransaction(db, {
|
|
1097
|
+
workspaceId: input.workspaceId,
|
|
1098
|
+
sessionId: input.sessionId,
|
|
1099
|
+
subjectId: input.subjectId,
|
|
1100
|
+
lock: true,
|
|
1101
|
+
});
|
|
1102
|
+
if (input.expectedDraftRevision !== null && input.expectedDraftRevision !== undefined) {
|
|
1103
|
+
const actualRevision = draft?.revision ?? 0;
|
|
1104
|
+
if (actualRevision !== input.expectedDraftRevision) {
|
|
1105
|
+
throw new QueueCommandConflictError("DRAFT_CHANGED", "Composer draft changed", {
|
|
1106
|
+
queueVersion: session.queueVersion,
|
|
1107
|
+
draftRevision: actualRevision,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
if (
|
|
1111
|
+
draft &&
|
|
1112
|
+
canonicalSessionCommandHash({
|
|
1113
|
+
text: draft.text,
|
|
1114
|
+
resources: draft.resources,
|
|
1115
|
+
tools: draft.tools,
|
|
1116
|
+
model: draft.model,
|
|
1117
|
+
reasoningEffort: draft.reasoningEffort,
|
|
1118
|
+
}) !==
|
|
1119
|
+
canonicalSessionCommandHash({
|
|
1120
|
+
text: input.text,
|
|
1121
|
+
resources: input.resources,
|
|
1122
|
+
tools: input.tools,
|
|
1123
|
+
model: input.model ?? session.model,
|
|
1124
|
+
reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
|
|
1125
|
+
})
|
|
1126
|
+
) {
|
|
1127
|
+
throw new QueueCommandConflictError(
|
|
1128
|
+
"DRAFT_CHANGED",
|
|
1129
|
+
"Submitted content is not the saved draft",
|
|
1130
|
+
{
|
|
1131
|
+
queueVersion: session.queueVersion,
|
|
1132
|
+
draftRevision: draft.revision,
|
|
1133
|
+
},
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
for (const update of input.mcpCredentialUpdates ?? []) {
|
|
1139
|
+
const [server] = await db
|
|
1140
|
+
.update(schema.sessionMcpServers)
|
|
1141
|
+
.set({
|
|
1142
|
+
headersEncrypted: update.headersEncrypted,
|
|
1143
|
+
credentialVersion: sql`${schema.sessionMcpServers.credentialVersion} + 1`,
|
|
1144
|
+
updatedAt: new Date(),
|
|
1145
|
+
})
|
|
1146
|
+
.where(
|
|
1147
|
+
and(
|
|
1148
|
+
eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
|
|
1149
|
+
eq(schema.sessionMcpServers.sessionId, input.sessionId),
|
|
1150
|
+
eq(schema.sessionMcpServers.serverId, update.id),
|
|
1151
|
+
),
|
|
1152
|
+
)
|
|
1153
|
+
.returning({ id: schema.sessionMcpServers.serverId });
|
|
1154
|
+
if (!server) throw new Error(`Unknown session MCP server: ${update.id}`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const now = new Date();
|
|
1158
|
+
const acceptedEventId = crypto.randomUUID();
|
|
1159
|
+
const turnId = crypto.randomUUID();
|
|
1160
|
+
const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
|
|
1161
|
+
let sequence = session.lastSequence;
|
|
1162
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [
|
|
1163
|
+
{
|
|
1164
|
+
id: acceptedEventId,
|
|
1165
|
+
accountId: input.accountId,
|
|
1166
|
+
workspaceId: input.workspaceId,
|
|
1167
|
+
sessionId: input.sessionId,
|
|
1168
|
+
sequence: ++sequence,
|
|
1169
|
+
type: "user.message",
|
|
1170
|
+
clientEventId: input.operationKey,
|
|
1171
|
+
payload: {
|
|
1172
|
+
text: input.text,
|
|
1173
|
+
...(input.resources.length ? { resources: input.resources } : {}),
|
|
1174
|
+
...(input.tools.length ? { tools: input.tools } : {}),
|
|
1175
|
+
...(input.model ? { model: input.model } : {}),
|
|
1176
|
+
...(input.reasoningEffort ? { reasoningEffort: input.reasoningEffort } : {}),
|
|
1177
|
+
delivery: input.delivery,
|
|
1178
|
+
},
|
|
1179
|
+
occurredAt: now,
|
|
1180
|
+
},
|
|
1181
|
+
];
|
|
1182
|
+
const existingQueued = await loadQueuedTurns(db, input.workspaceId, input.sessionId, true);
|
|
1183
|
+
const [turn] = await db
|
|
1184
|
+
.insert(schema.sessionTurns)
|
|
1185
|
+
.values({
|
|
1186
|
+
id: turnId,
|
|
1187
|
+
accountId: input.accountId,
|
|
1188
|
+
workspaceId: input.workspaceId,
|
|
1189
|
+
sessionId: input.sessionId,
|
|
1190
|
+
triggerEventId: acceptedEventId,
|
|
1191
|
+
temporalWorkflowId: workflowId,
|
|
1192
|
+
status: "queued",
|
|
1193
|
+
source: input.source,
|
|
1194
|
+
position: input.delivery === "steer" ? 0 : existingQueued.length + 1,
|
|
1195
|
+
prompt: input.text,
|
|
1196
|
+
resources: input.resources,
|
|
1197
|
+
tools: input.tools,
|
|
1198
|
+
model: input.model ?? session.model,
|
|
1199
|
+
reasoningEffort: input.reasoningEffort ?? input.reasoningEffortFallback,
|
|
1200
|
+
sandboxBackend: session.sandboxBackend,
|
|
1201
|
+
metadata: {},
|
|
1202
|
+
lineage: { actor: input.actor.type },
|
|
1203
|
+
})
|
|
1204
|
+
.returning();
|
|
1205
|
+
if (!turn) throw new SessionControlInvariantError("Prompt turn was not inserted");
|
|
1206
|
+
eventValues.push({
|
|
1207
|
+
accountId: input.accountId,
|
|
1208
|
+
workspaceId: input.workspaceId,
|
|
1209
|
+
sessionId: input.sessionId,
|
|
1210
|
+
sequence: ++sequence,
|
|
1211
|
+
type: "turn.queued",
|
|
1212
|
+
turnId,
|
|
1213
|
+
payload: { turnId, triggerEventId: acceptedEventId, source: input.source },
|
|
1214
|
+
occurredAt: now,
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
const supersession =
|
|
1218
|
+
input.delivery === "steer"
|
|
1219
|
+
? await supersedeSessionCurrentDirectionInTransaction(db, {
|
|
1220
|
+
accountId: input.accountId,
|
|
1221
|
+
workspaceId: input.workspaceId,
|
|
1222
|
+
sessionId: input.sessionId,
|
|
1223
|
+
activeTurnId: session.activeTurnId,
|
|
1224
|
+
actor: input.actor,
|
|
1225
|
+
operationId: reserved.receipt.id,
|
|
1226
|
+
controlRevision: resumed.revision,
|
|
1227
|
+
lastSequence: session.lastSequence,
|
|
1228
|
+
})
|
|
1229
|
+
: {
|
|
1230
|
+
interruptionCount: 0,
|
|
1231
|
+
replacedTurn: null,
|
|
1232
|
+
liveCurrentTurnId: null,
|
|
1233
|
+
lastSequence: session.lastSequence,
|
|
1234
|
+
};
|
|
1235
|
+
// Ownerless Steer settlement may have appended interrupted tool results.
|
|
1236
|
+
// Rebase the not-yet-inserted foreground events after those canonical rows.
|
|
1237
|
+
sequence = supersession.lastSequence;
|
|
1238
|
+
for (const event of eventValues) event.sequence = ++sequence;
|
|
1239
|
+
const interruptionCount = supersession.interruptionCount;
|
|
1240
|
+
const replacedTurnId = supersession.replacedTurn?.id ?? null;
|
|
1241
|
+
const liveCurrentTurnId = supersession.liveCurrentTurnId;
|
|
1242
|
+
if (supersession.replacedTurn) {
|
|
1243
|
+
const current = supersession.replacedTurn;
|
|
1244
|
+
if (!liveCurrentTurnId) {
|
|
1245
|
+
eventValues.push({
|
|
1246
|
+
accountId: input.accountId,
|
|
1247
|
+
workspaceId: input.workspaceId,
|
|
1248
|
+
sessionId: input.sessionId,
|
|
1249
|
+
sequence: ++sequence,
|
|
1250
|
+
type: "turn.superseded",
|
|
1251
|
+
turnId: current.id,
|
|
1252
|
+
payload: { reason: "steer", targetTurnId: turnId },
|
|
1253
|
+
occurredAt: now,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
eventValues.push({
|
|
1257
|
+
accountId: input.accountId,
|
|
1258
|
+
workspaceId: input.workspaceId,
|
|
1259
|
+
sessionId: input.sessionId,
|
|
1260
|
+
sequence: ++sequence,
|
|
1261
|
+
type: "session.control.steer_requested",
|
|
1262
|
+
turnId: current.id,
|
|
1263
|
+
turnGeneration: current.executionGeneration,
|
|
1264
|
+
turnAttemptId: current.activeAttemptId,
|
|
1265
|
+
turnAssociation: "current",
|
|
1266
|
+
payload: {
|
|
1267
|
+
operationId: reserved.receipt.id,
|
|
1268
|
+
targetTurnId: turnId,
|
|
1269
|
+
replacedTurnId: current.id,
|
|
1270
|
+
stopping: liveCurrentTurnId !== null,
|
|
1271
|
+
},
|
|
1272
|
+
occurredAt: now,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const ordered =
|
|
1277
|
+
input.delivery === "steer" ? [turn, ...existingQueued] : [...existingQueued, turn];
|
|
1278
|
+
await normalizeQueuePositions(
|
|
1279
|
+
db,
|
|
1280
|
+
input.workspaceId,
|
|
1281
|
+
input.sessionId,
|
|
1282
|
+
ordered.map((row) => row.id),
|
|
1283
|
+
);
|
|
1284
|
+
const noCurrentAfter =
|
|
1285
|
+
input.delivery === "steer" ? liveCurrentTurnId === null : !session.activeTurnId;
|
|
1286
|
+
const nextStatus = noCurrentAfter ? "queued" : session.status;
|
|
1287
|
+
if (nextStatus !== session.status) {
|
|
1288
|
+
eventValues.push({
|
|
1289
|
+
accountId: input.accountId,
|
|
1290
|
+
workspaceId: input.workspaceId,
|
|
1291
|
+
sessionId: input.sessionId,
|
|
1292
|
+
sequence: ++sequence,
|
|
1293
|
+
type: "session.status.changed",
|
|
1294
|
+
payload: { status: nextStatus },
|
|
1295
|
+
occurredAt: now,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
const eventRows = await db.insert(schema.sessionEvents).values(eventValues).returning();
|
|
1299
|
+
const queueVersion = session.queueVersion + 1;
|
|
1300
|
+
await db
|
|
1301
|
+
.update(schema.sessions)
|
|
1302
|
+
.set({
|
|
1303
|
+
resources: mergeResourceRefs(session.resources as ResourceRef[], input.resources),
|
|
1304
|
+
tools: mergeToolRefs(session.tools as ToolRef[], input.tools),
|
|
1305
|
+
activeTurnId: input.delivery === "steer" ? liveCurrentTurnId : session.activeTurnId,
|
|
1306
|
+
status: nextStatus,
|
|
1307
|
+
queueVersion,
|
|
1308
|
+
queueHeadPosition: 0,
|
|
1309
|
+
queueTailPosition: ordered.length,
|
|
1310
|
+
lastSequence: sequence,
|
|
1311
|
+
updatedAt: now,
|
|
1312
|
+
})
|
|
1313
|
+
.where(eq(schema.sessions.id, input.sessionId));
|
|
1314
|
+
if (draft) {
|
|
1315
|
+
await db.delete(schema.composerDrafts).where(eq(schema.composerDrafts.id, draft.id));
|
|
1316
|
+
}
|
|
1317
|
+
const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
|
|
1318
|
+
accountId: input.accountId,
|
|
1319
|
+
workspaceId: input.workspaceId,
|
|
1320
|
+
sessionId: input.sessionId,
|
|
1321
|
+
temporalWorkflowId: workflowId,
|
|
1322
|
+
reason: input.delivery === "steer" ? "prompt_steer" : "prompt_send",
|
|
1323
|
+
});
|
|
1324
|
+
await db.insert(schema.auditEvents).values({
|
|
1325
|
+
accountId: input.accountId,
|
|
1326
|
+
workspaceId: input.workspaceId,
|
|
1327
|
+
subjectId:
|
|
1328
|
+
input.actor.type === "agent_attempt"
|
|
1329
|
+
? `attempt:${input.actor.attemptId}`
|
|
1330
|
+
: input.actor.subjectId,
|
|
1331
|
+
action: input.delivery === "steer" ? "session.prompt.steer" : "session.prompt.send",
|
|
1332
|
+
targetType: "session_turn",
|
|
1333
|
+
targetId: turnId,
|
|
1334
|
+
metadata: { operationId: reserved.receipt.id, replacedTurnId, interruptionCount },
|
|
1335
|
+
});
|
|
1336
|
+
const eventIds = eventRows.map((event) => event.id);
|
|
1337
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1338
|
+
controlRevision: resumed.revision,
|
|
1339
|
+
queueVersion,
|
|
1340
|
+
turnVersion: turn.version,
|
|
1341
|
+
...(draft ? { draftRevision: draft.revision } : {}),
|
|
1342
|
+
result: {
|
|
1343
|
+
turnId,
|
|
1344
|
+
acceptedEventId,
|
|
1345
|
+
eventIds,
|
|
1346
|
+
wakeRevision,
|
|
1347
|
+
interruptionCount,
|
|
1348
|
+
replacedTurnId,
|
|
1349
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
return {
|
|
1353
|
+
receipt,
|
|
1354
|
+
queueVersion,
|
|
1355
|
+
acceptedEventId,
|
|
1356
|
+
eventIds,
|
|
1357
|
+
turnId,
|
|
1358
|
+
wakeRevision,
|
|
1359
|
+
interruptionCount,
|
|
1360
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
1361
|
+
replay: false,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export async function sendAgentMessageInTransaction(
|
|
1366
|
+
db: Database,
|
|
1367
|
+
input: {
|
|
1368
|
+
accountId: string;
|
|
1369
|
+
workspaceId: string;
|
|
1370
|
+
targetSessionId: string;
|
|
1371
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
1372
|
+
operationKey: string;
|
|
1373
|
+
text: string;
|
|
1374
|
+
},
|
|
1375
|
+
): Promise<AgentInternalUpdateCommandResult> {
|
|
1376
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "share");
|
|
1377
|
+
const requestHash = canonicalSessionCommandHash({ text: input.text });
|
|
1378
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
1379
|
+
accountId: input.accountId,
|
|
1380
|
+
workspaceId: input.workspaceId,
|
|
1381
|
+
actor: input.actor,
|
|
1382
|
+
action: "agent.message",
|
|
1383
|
+
targetSessionId: input.targetSessionId,
|
|
1384
|
+
targetTurnId: null,
|
|
1385
|
+
operationKey: input.operationKey,
|
|
1386
|
+
canonicalRequestHash: requestHash,
|
|
1387
|
+
});
|
|
1388
|
+
if (reserved.replay) {
|
|
1389
|
+
const updateId = String(reserved.receipt.result.updateId ?? "");
|
|
1390
|
+
const workflowId = String(reserved.receipt.result.workflowId ?? "");
|
|
1391
|
+
if (!updateId || !workflowId) {
|
|
1392
|
+
throw new SessionControlInvariantError("Replayed Agent message receipt is incomplete");
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
receipt: reserved.receipt,
|
|
1396
|
+
updateId,
|
|
1397
|
+
eventIds: Array.isArray(reserved.receipt.result.eventIds)
|
|
1398
|
+
? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
|
|
1399
|
+
: [],
|
|
1400
|
+
wakeRevision:
|
|
1401
|
+
typeof reserved.receipt.result.wakeRevision === "number"
|
|
1402
|
+
? reserved.receipt.result.wakeRevision
|
|
1403
|
+
: null,
|
|
1404
|
+
shouldSignal: false,
|
|
1405
|
+
workflowId,
|
|
1406
|
+
effectiveState: reserved.receipt.result.effectiveState === "paused" ? "paused" : "active",
|
|
1407
|
+
interruptionCount: 0,
|
|
1408
|
+
workspaceControlEventId: null,
|
|
1409
|
+
replay: true,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
await assertAgentCommandAuthorityInTransaction(db, {
|
|
1413
|
+
workspaceId: input.workspaceId,
|
|
1414
|
+
actor: input.actor,
|
|
1415
|
+
targetSessionId: input.targetSessionId,
|
|
1416
|
+
action: "message",
|
|
1417
|
+
});
|
|
1418
|
+
const session = await lockSession(db, input.workspaceId, input.targetSessionId);
|
|
1419
|
+
if (session.status === "cancelled") {
|
|
1420
|
+
throw new QueueCommandConflictError(
|
|
1421
|
+
"QUEUE_PROMPT_STARTED",
|
|
1422
|
+
"Cancelled session cannot accept an Agent message",
|
|
1423
|
+
{ queueVersion: session.queueVersion },
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
const effective = await evaluateSessionControl(db, input.workspaceId, input.targetSessionId, {
|
|
1427
|
+
lock: "share",
|
|
1428
|
+
});
|
|
1429
|
+
const now = new Date();
|
|
1430
|
+
const [update] = await db
|
|
1431
|
+
.insert(schema.sessionSystemUpdates)
|
|
1432
|
+
.values({
|
|
1433
|
+
accountId: input.accountId,
|
|
1434
|
+
workspaceId: input.workspaceId,
|
|
1435
|
+
sessionId: input.targetSessionId,
|
|
1436
|
+
kind: "agent_message",
|
|
1437
|
+
classification: "info",
|
|
1438
|
+
sourceId: input.actor.sessionId,
|
|
1439
|
+
dedupeKey: `agent-message:${reserved.receipt.id}`,
|
|
1440
|
+
summary: input.text,
|
|
1441
|
+
payload: {
|
|
1442
|
+
type: "agent_message",
|
|
1443
|
+
text: input.text,
|
|
1444
|
+
operationId: reserved.receipt.id,
|
|
1445
|
+
},
|
|
1446
|
+
lineage: {
|
|
1447
|
+
callerSessionId: input.actor.sessionId,
|
|
1448
|
+
callerTurnId: input.actor.turnId,
|
|
1449
|
+
callerAttemptId: input.actor.attemptId,
|
|
1450
|
+
callerExecutionGeneration: input.actor.executionGeneration,
|
|
1451
|
+
},
|
|
1452
|
+
state: "pending",
|
|
1453
|
+
})
|
|
1454
|
+
.returning({ id: schema.sessionSystemUpdates.id });
|
|
1455
|
+
if (!update) throw new SessionControlInvariantError("Agent message was not inserted");
|
|
1456
|
+
const [event] = await db
|
|
1457
|
+
.insert(schema.sessionEvents)
|
|
1458
|
+
.values({
|
|
1459
|
+
accountId: input.accountId,
|
|
1460
|
+
workspaceId: input.workspaceId,
|
|
1461
|
+
sessionId: input.targetSessionId,
|
|
1462
|
+
sequence: session.lastSequence + 1,
|
|
1463
|
+
type: "system.update.pending",
|
|
1464
|
+
payload: {
|
|
1465
|
+
updateId: update.id,
|
|
1466
|
+
kind: "agent_message",
|
|
1467
|
+
sourceSessionId: input.actor.sessionId,
|
|
1468
|
+
},
|
|
1469
|
+
occurredAt: now,
|
|
1470
|
+
})
|
|
1471
|
+
.returning({ id: schema.sessionEvents.id });
|
|
1472
|
+
if (!event) throw new SessionControlInvariantError("Agent message event was not inserted");
|
|
1473
|
+
const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
|
|
1474
|
+
const runnable = session.activeTurnId === null && effective.state === "active";
|
|
1475
|
+
const wake = runnable
|
|
1476
|
+
? await registerInternalUpdateWakeInTransaction(db, {
|
|
1477
|
+
accountId: input.accountId,
|
|
1478
|
+
workspaceId: input.workspaceId,
|
|
1479
|
+
sessionId: input.targetSessionId,
|
|
1480
|
+
temporalWorkflowId: workflowId,
|
|
1481
|
+
})
|
|
1482
|
+
: null;
|
|
1483
|
+
await db
|
|
1484
|
+
.update(schema.sessions)
|
|
1485
|
+
.set({
|
|
1486
|
+
lastSequence: session.lastSequence + 1,
|
|
1487
|
+
...(runnable ? { status: "queued" as const } : {}),
|
|
1488
|
+
updatedAt: now,
|
|
1489
|
+
})
|
|
1490
|
+
.where(eq(schema.sessions.id, input.targetSessionId));
|
|
1491
|
+
await db.insert(schema.auditEvents).values({
|
|
1492
|
+
accountId: input.accountId,
|
|
1493
|
+
workspaceId: input.workspaceId,
|
|
1494
|
+
subjectId: `attempt:${input.actor.attemptId}`,
|
|
1495
|
+
action: "session.agent_message",
|
|
1496
|
+
targetType: "session",
|
|
1497
|
+
targetId: input.targetSessionId,
|
|
1498
|
+
metadata: {
|
|
1499
|
+
operationId: reserved.receipt.id,
|
|
1500
|
+
callerSessionId: input.actor.sessionId,
|
|
1501
|
+
callerTurnId: input.actor.turnId,
|
|
1502
|
+
callerAttemptId: input.actor.attemptId,
|
|
1503
|
+
callerExecutionGeneration: input.actor.executionGeneration,
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1506
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1507
|
+
result: {
|
|
1508
|
+
updateId: update.id,
|
|
1509
|
+
eventIds: [event.id],
|
|
1510
|
+
wakeRevision: wake?.wakeRevision ?? null,
|
|
1511
|
+
workflowId,
|
|
1512
|
+
effectiveState: effective.state,
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
return {
|
|
1516
|
+
receipt,
|
|
1517
|
+
updateId: update.id,
|
|
1518
|
+
eventIds: [event.id],
|
|
1519
|
+
wakeRevision: wake?.wakeRevision ?? null,
|
|
1520
|
+
shouldSignal: wake?.shouldSignal ?? false,
|
|
1521
|
+
workflowId,
|
|
1522
|
+
effectiveState: effective.state,
|
|
1523
|
+
interruptionCount: 0,
|
|
1524
|
+
workspaceControlEventId: null,
|
|
1525
|
+
replay: false,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
export async function steerAgentSessionInTransaction(
|
|
1530
|
+
db: Database,
|
|
1531
|
+
input: {
|
|
1532
|
+
accountId: string;
|
|
1533
|
+
workspaceId: string;
|
|
1534
|
+
targetSessionId: string;
|
|
1535
|
+
actor: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
1536
|
+
operationKey: string;
|
|
1537
|
+
instruction: string;
|
|
1538
|
+
},
|
|
1539
|
+
): Promise<AgentInternalUpdateCommandResult> {
|
|
1540
|
+
await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
1541
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
1542
|
+
accountId: input.accountId,
|
|
1543
|
+
workspaceId: input.workspaceId,
|
|
1544
|
+
actor: input.actor,
|
|
1545
|
+
action: "agent.steer",
|
|
1546
|
+
targetSessionId: input.targetSessionId,
|
|
1547
|
+
targetTurnId: null,
|
|
1548
|
+
operationKey: input.operationKey,
|
|
1549
|
+
canonicalRequestHash: canonicalSessionCommandHash({ instruction: input.instruction }),
|
|
1550
|
+
});
|
|
1551
|
+
if (reserved.replay) {
|
|
1552
|
+
const updateId = String(reserved.receipt.result.updateId ?? "");
|
|
1553
|
+
const workflowId = String(reserved.receipt.result.workflowId ?? "");
|
|
1554
|
+
const wakeRevision = Number(reserved.receipt.result.wakeRevision ?? 0);
|
|
1555
|
+
if (!updateId || !workflowId || wakeRevision < 1) {
|
|
1556
|
+
throw new SessionControlInvariantError("Replayed Agent Steer receipt is incomplete");
|
|
1557
|
+
}
|
|
1558
|
+
return {
|
|
1559
|
+
receipt: reserved.receipt,
|
|
1560
|
+
updateId,
|
|
1561
|
+
eventIds: Array.isArray(reserved.receipt.result.eventIds)
|
|
1562
|
+
? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
|
|
1563
|
+
: [],
|
|
1564
|
+
wakeRevision,
|
|
1565
|
+
shouldSignal: false,
|
|
1566
|
+
workflowId,
|
|
1567
|
+
effectiveState: "active",
|
|
1568
|
+
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
1569
|
+
workspaceControlEventId:
|
|
1570
|
+
typeof reserved.receipt.result.workspaceControlEventId === "string"
|
|
1571
|
+
? reserved.receipt.result.workspaceControlEventId
|
|
1572
|
+
: null,
|
|
1573
|
+
replay: true,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
await assertAgentCommandAuthorityInTransaction(db, {
|
|
1577
|
+
workspaceId: input.workspaceId,
|
|
1578
|
+
actor: input.actor,
|
|
1579
|
+
targetSessionId: input.targetSessionId,
|
|
1580
|
+
action: "steer",
|
|
1581
|
+
});
|
|
1582
|
+
const resumed = await autoResumeSessionBranchInTransaction(db, {
|
|
1583
|
+
workspaceId: input.workspaceId,
|
|
1584
|
+
sessionId: input.targetSessionId,
|
|
1585
|
+
actor: `attempt:${input.actor.attemptId}`,
|
|
1586
|
+
reason: "agent_steer",
|
|
1587
|
+
});
|
|
1588
|
+
const session = await lockSession(db, input.workspaceId, input.targetSessionId);
|
|
1589
|
+
if (session.status === "cancelled") {
|
|
1590
|
+
throw new QueueCommandConflictError(
|
|
1591
|
+
"QUEUE_PROMPT_STARTED",
|
|
1592
|
+
"Cancelled session cannot be Steered",
|
|
1593
|
+
{ queueVersion: session.queueVersion },
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const supersession = await supersedeSessionCurrentDirectionInTransaction(db, {
|
|
1597
|
+
accountId: input.accountId,
|
|
1598
|
+
workspaceId: input.workspaceId,
|
|
1599
|
+
sessionId: input.targetSessionId,
|
|
1600
|
+
activeTurnId: session.activeTurnId,
|
|
1601
|
+
actor: input.actor,
|
|
1602
|
+
operationId: reserved.receipt.id,
|
|
1603
|
+
controlRevision: resumed.revision,
|
|
1604
|
+
lastSequence: session.lastSequence,
|
|
1605
|
+
});
|
|
1606
|
+
await db
|
|
1607
|
+
.update(schema.sessionSystemUpdates)
|
|
1608
|
+
.set({ state: "superseded" })
|
|
1609
|
+
.where(
|
|
1610
|
+
and(
|
|
1611
|
+
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
1612
|
+
eq(schema.sessionSystemUpdates.sessionId, input.targetSessionId),
|
|
1613
|
+
eq(schema.sessionSystemUpdates.kind, "agent_steer_instruction"),
|
|
1614
|
+
inArray(schema.sessionSystemUpdates.state, ["pending", "deferred"]),
|
|
1615
|
+
),
|
|
1616
|
+
);
|
|
1617
|
+
const now = new Date();
|
|
1618
|
+
const [update] = await db
|
|
1619
|
+
.insert(schema.sessionSystemUpdates)
|
|
1620
|
+
.values({
|
|
1621
|
+
accountId: input.accountId,
|
|
1622
|
+
workspaceId: input.workspaceId,
|
|
1623
|
+
sessionId: input.targetSessionId,
|
|
1624
|
+
kind: "agent_steer_instruction",
|
|
1625
|
+
classification: "action_required",
|
|
1626
|
+
sourceId: input.actor.sessionId,
|
|
1627
|
+
dedupeKey: `agent-steer:${reserved.receipt.id}`,
|
|
1628
|
+
summary: input.instruction,
|
|
1629
|
+
payload: {
|
|
1630
|
+
type: "agent_steer_instruction",
|
|
1631
|
+
instruction: input.instruction,
|
|
1632
|
+
operationId: reserved.receipt.id,
|
|
1633
|
+
},
|
|
1634
|
+
lineage: {
|
|
1635
|
+
callerSessionId: input.actor.sessionId,
|
|
1636
|
+
callerTurnId: input.actor.turnId,
|
|
1637
|
+
callerAttemptId: input.actor.attemptId,
|
|
1638
|
+
callerExecutionGeneration: input.actor.executionGeneration,
|
|
1639
|
+
},
|
|
1640
|
+
state: "pending",
|
|
1641
|
+
})
|
|
1642
|
+
.returning({ id: schema.sessionSystemUpdates.id });
|
|
1643
|
+
if (!update) throw new SessionControlInvariantError("Agent Steer instruction was not inserted");
|
|
1644
|
+
let sequence = supersession.lastSequence;
|
|
1645
|
+
const events: Array<typeof schema.sessionEvents.$inferInsert> = [];
|
|
1646
|
+
if (supersession.replacedTurn && !supersession.liveCurrentTurnId) {
|
|
1647
|
+
events.push({
|
|
1648
|
+
accountId: input.accountId,
|
|
1649
|
+
workspaceId: input.workspaceId,
|
|
1650
|
+
sessionId: input.targetSessionId,
|
|
1651
|
+
sequence: ++sequence,
|
|
1652
|
+
type: "turn.superseded",
|
|
1653
|
+
turnId: supersession.replacedTurn.id,
|
|
1654
|
+
payload: { reason: "agent_steer", targetUpdateId: update.id },
|
|
1655
|
+
occurredAt: now,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
events.push(
|
|
1659
|
+
{
|
|
1660
|
+
accountId: input.accountId,
|
|
1661
|
+
workspaceId: input.workspaceId,
|
|
1662
|
+
sessionId: input.targetSessionId,
|
|
1663
|
+
sequence: ++sequence,
|
|
1664
|
+
type: "session.control.steer_requested",
|
|
1665
|
+
turnId: supersession.replacedTurn?.id ?? null,
|
|
1666
|
+
turnGeneration: supersession.replacedTurn?.executionGeneration ?? null,
|
|
1667
|
+
turnAttemptId: supersession.replacedTurn?.activeAttemptId ?? null,
|
|
1668
|
+
turnAssociation: supersession.replacedTurn ? "current" : null,
|
|
1669
|
+
payload: {
|
|
1670
|
+
operationId: reserved.receipt.id,
|
|
1671
|
+
targetUpdateId: update.id,
|
|
1672
|
+
replacedTurnId: supersession.replacedTurn?.id ?? null,
|
|
1673
|
+
actorSessionId: input.actor.sessionId,
|
|
1674
|
+
stopping: supersession.liveCurrentTurnId !== null,
|
|
1675
|
+
},
|
|
1676
|
+
occurredAt: now,
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
accountId: input.accountId,
|
|
1680
|
+
workspaceId: input.workspaceId,
|
|
1681
|
+
sessionId: input.targetSessionId,
|
|
1682
|
+
sequence: ++sequence,
|
|
1683
|
+
type: "system.update.pending",
|
|
1684
|
+
payload: {
|
|
1685
|
+
updateId: update.id,
|
|
1686
|
+
kind: "agent_steer_instruction",
|
|
1687
|
+
sourceSessionId: input.actor.sessionId,
|
|
1688
|
+
},
|
|
1689
|
+
occurredAt: now,
|
|
1690
|
+
},
|
|
1691
|
+
);
|
|
1692
|
+
const insertedEvents = await db.insert(schema.sessionEvents).values(events).returning({
|
|
1693
|
+
id: schema.sessionEvents.id,
|
|
1694
|
+
});
|
|
1695
|
+
const workflowId = session.temporalWorkflowId ?? `session-${session.id}`;
|
|
1696
|
+
const wakeRevision = await registerSessionWorkflowWakeInTransaction(db, {
|
|
1697
|
+
accountId: input.accountId,
|
|
1698
|
+
workspaceId: input.workspaceId,
|
|
1699
|
+
sessionId: input.targetSessionId,
|
|
1700
|
+
temporalWorkflowId: workflowId,
|
|
1701
|
+
reason: "agent_steer",
|
|
1702
|
+
});
|
|
1703
|
+
await db
|
|
1704
|
+
.update(schema.sessions)
|
|
1705
|
+
.set({
|
|
1706
|
+
activeTurnId: supersession.liveCurrentTurnId,
|
|
1707
|
+
status: supersession.liveCurrentTurnId ? session.status : "queued",
|
|
1708
|
+
lastSequence: sequence,
|
|
1709
|
+
updatedAt: now,
|
|
1710
|
+
})
|
|
1711
|
+
.where(eq(schema.sessions.id, input.targetSessionId));
|
|
1712
|
+
await db.insert(schema.auditEvents).values({
|
|
1713
|
+
accountId: input.accountId,
|
|
1714
|
+
workspaceId: input.workspaceId,
|
|
1715
|
+
subjectId: `attempt:${input.actor.attemptId}`,
|
|
1716
|
+
action: "session.agent_steer",
|
|
1717
|
+
targetType: "session",
|
|
1718
|
+
targetId: input.targetSessionId,
|
|
1719
|
+
metadata: {
|
|
1720
|
+
operationId: reserved.receipt.id,
|
|
1721
|
+
callerSessionId: input.actor.sessionId,
|
|
1722
|
+
callerTurnId: input.actor.turnId,
|
|
1723
|
+
callerAttemptId: input.actor.attemptId,
|
|
1724
|
+
callerExecutionGeneration: input.actor.executionGeneration,
|
|
1725
|
+
controlRevision: resumed.revision,
|
|
1726
|
+
interruptionCount: supersession.interruptionCount,
|
|
1727
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
1728
|
+
},
|
|
1729
|
+
});
|
|
1730
|
+
const eventIds = insertedEvents.map((event) => event.id);
|
|
1731
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1732
|
+
controlRevision: resumed.revision,
|
|
1733
|
+
result: {
|
|
1734
|
+
updateId: update.id,
|
|
1735
|
+
eventIds,
|
|
1736
|
+
wakeRevision,
|
|
1737
|
+
workflowId,
|
|
1738
|
+
interruptionCount: supersession.interruptionCount,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
return {
|
|
1742
|
+
receipt,
|
|
1743
|
+
updateId: update.id,
|
|
1744
|
+
eventIds,
|
|
1745
|
+
wakeRevision,
|
|
1746
|
+
shouldSignal: true,
|
|
1747
|
+
workflowId,
|
|
1748
|
+
effectiveState: "active",
|
|
1749
|
+
interruptionCount: supersession.interruptionCount,
|
|
1750
|
+
workspaceControlEventId: resumed.workspaceControlEventId,
|
|
1751
|
+
replay: false,
|
|
1752
|
+
};
|
|
1753
|
+
}
|