@opengeni/db 4.0.0-canary.0 → 4.0.0-canary.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/canonical-human-identities.js +3 -3
- package/dist/{chunk-2XEZK4LF.js → chunk-7YZP2CSL.js} +23 -1
- package/dist/chunk-7YZP2CSL.js.map +1 -0
- package/dist/{chunk-P6NSEG2S.js → chunk-B5Y3TCSP.js} +2 -2
- package/dist/{chunk-CJROYHSC.js → chunk-CQF6G4IF.js} +3 -3
- package/dist/chunk-DUB3LUVM.js +4145 -0
- package/dist/chunk-DUB3LUVM.js.map +1 -0
- package/dist/{chunk-3RI33YQN.js → chunk-FRIYCNK2.js} +3 -1
- package/dist/{chunk-3RI33YQN.js.map → chunk-FRIYCNK2.js.map} +1 -1
- package/dist/{chunk-RZNOSEMC.js → chunk-L2QFSPFP.js} +2 -2
- package/dist/{chunk-NLFHMPUB.js → chunk-NPKBYGAK.js} +2 -2
- package/dist/{chunk-QM6YOWHZ.js → chunk-PWJJKCTF.js} +85 -4
- package/dist/chunk-PWJJKCTF.js.map +1 -0
- package/dist/{chunk-MX6V5F65.js → chunk-VU2IK37Y.js} +2 -2
- package/dist/{chunk-UFAMOUWL.js → chunk-XIYAXH47.js} +2 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +113 -7
- package/dist/index.js +9392 -11506
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/migrate.d.ts +6 -0
- package/dist/migrate.js +3 -1
- package/dist/migration-runner-settings.d.ts +10 -0
- package/dist/new-session-drafts.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +270 -9
- package/dist/schema.js +3 -1
- package/dist/session-background-commands.d.ts +83 -0
- package/dist/session-background-commands.js +18 -3
- package/dist/session-command-output.d.ts +17 -0
- package/dist/session-command-output.js +130 -0
- package/dist/session-command-output.js.map +1 -0
- package/dist/session-event-slices.d.ts +28 -0
- package/dist/session-event-slices.js +225 -0
- package/dist/session-event-slices.js.map +1 -0
- package/dist/session-mcp-progress.d.ts +12 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/site-publication-request.d.ts +18 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +34 -4
- package/dist/xai-subscription.d.ts +2 -0
- package/drizzle/0407_connected_command_tracking_retirement.sql +245 -0
- package/drizzle/0408_scheduled_session_target_index.sql +4 -0
- package/drizzle/0409_new_session_draft_project_provenance.sql +321 -0
- package/drizzle/0410_new_session_draft_project_provenance_index.sql +5 -0
- package/drizzle/0411_new_session_draft_project_provenance_backfill.sql +53 -0
- package/drizzle/0412_new_session_draft_project_provenance_validation.sql +30 -0
- package/drizzle/0413_session_filter_activity.sql +96 -0
- package/drizzle/0414_scheduled_generated_producer_materialization.sql +287 -0
- package/drizzle/0415_command_completion_observation.sql +9 -0
- package/drizzle/0416_scheduled_inherited_tool_admission.sql +27 -0
- package/drizzle/0417_command_runner_failure_metadata.sql +17 -0
- package/drizzle/0418_site_direct_uploads.sql +151 -0
- package/drizzle/0419_background_command_launch_authority.sql +56 -0
- package/package.json +14 -6
- package/src/index.ts +1691 -217
- package/src/migrate.ts +10 -2
- package/src/migration-runner-settings.ts +21 -0
- package/src/new-session-drafts.ts +105 -1
- package/src/runtime-posture.ts +2 -0
- package/src/schema.ts +97 -3
- package/src/session-background-commands.ts +320 -4
- package/src/session-command-output.ts +158 -0
- package/src/session-control.ts +53 -21
- package/src/session-event-slices.ts +310 -0
- package/src/session-mcp-progress.ts +67 -0
- package/src/site-publication-request.ts +32 -0
- package/src/workspace-artifacts.ts +95 -96
- package/src/xai-subscription.ts +8 -0
- package/dist/chunk-2XEZK4LF.js.map +0 -1
- package/dist/chunk-N5VXJ4I2.js +0 -589
- package/dist/chunk-N5VXJ4I2.js.map +0 -1
- package/dist/chunk-QM6YOWHZ.js.map +0 -1
- /package/dist/{chunk-P6NSEG2S.js.map → chunk-B5Y3TCSP.js.map} +0 -0
- /package/dist/{chunk-CJROYHSC.js.map → chunk-CQF6G4IF.js.map} +0 -0
- /package/dist/{chunk-RZNOSEMC.js.map → chunk-L2QFSPFP.js.map} +0 -0
- /package/dist/{chunk-NLFHMPUB.js.map → chunk-NPKBYGAK.js.map} +0 -0
- /package/dist/{chunk-MX6V5F65.js.map → chunk-VU2IK37Y.js.map} +0 -0
- /package/dist/{chunk-UFAMOUWL.js.map → chunk-XIYAXH47.js.map} +0 -0
|
@@ -0,0 +1,4145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancelUnacceptedVideoGenerationsForToolCallsInTransaction
|
|
3
|
+
} from "./chunk-CQF6G4IF.js";
|
|
4
|
+
import {
|
|
5
|
+
nestedPostgresSqlState,
|
|
6
|
+
withRlsContext,
|
|
7
|
+
withSessionActivityRlsContext
|
|
8
|
+
} from "./chunk-XIYAXH47.js";
|
|
9
|
+
import {
|
|
10
|
+
LOSSLESS_CONTENT_CODEC_VERSION,
|
|
11
|
+
auditEvents,
|
|
12
|
+
codexCapacityWaiters,
|
|
13
|
+
fromPostgresLosslessJson,
|
|
14
|
+
sandboxRetainedProcesses,
|
|
15
|
+
sessionAttemptInterruptions,
|
|
16
|
+
sessionBackgroundCommands,
|
|
17
|
+
sessionCommandReceipts,
|
|
18
|
+
sessionEventCursors,
|
|
19
|
+
sessionEvents,
|
|
20
|
+
sessionHistoryItems,
|
|
21
|
+
sessionHumanInputRequests,
|
|
22
|
+
sessionPendingToolCalls,
|
|
23
|
+
sessionRealtimeEntries,
|
|
24
|
+
sessionRealtimeModes,
|
|
25
|
+
sessionSystemUpdateOutbox,
|
|
26
|
+
sessionSystemUpdates,
|
|
27
|
+
sessionTurnAttempts,
|
|
28
|
+
sessionTurns,
|
|
29
|
+
sessionWorkflowWakeOutbox,
|
|
30
|
+
sessions,
|
|
31
|
+
withLosslessContentWriteVersion,
|
|
32
|
+
workspaceControlEvents,
|
|
33
|
+
workspaceInferenceControls,
|
|
34
|
+
workspaces
|
|
35
|
+
} from "./chunk-PWJJKCTF.js";
|
|
36
|
+
|
|
37
|
+
// src/session-background-commands.ts
|
|
38
|
+
import { SessionCommandFailure } from "@opengeni/contracts";
|
|
39
|
+
import { isDeepStrictEqual } from "util";
|
|
40
|
+
import { and as and4, asc as asc2, desc as desc2, eq as eq4, gte, inArray as inArray3, sql as sql4 } from "drizzle-orm";
|
|
41
|
+
|
|
42
|
+
// src/session-control.ts
|
|
43
|
+
import { createHash as createHash2 } from "crypto";
|
|
44
|
+
import {
|
|
45
|
+
boundWorkspaceControlEvent,
|
|
46
|
+
childPausedClassification,
|
|
47
|
+
McpPersonalConnectionDelegations,
|
|
48
|
+
workspaceControlUtf8Bytes
|
|
49
|
+
} from "@opengeni/contracts";
|
|
50
|
+
import { and as and3, eq as eq3, inArray as inArray2, sql as sql3 } from "drizzle-orm";
|
|
51
|
+
|
|
52
|
+
// src/child-lifecycle-notices.ts
|
|
53
|
+
import {
|
|
54
|
+
CHILD_PAUSED_REASON_MAX_BYTES,
|
|
55
|
+
CHILD_PROGRESS_NOTE_MAX_BYTES,
|
|
56
|
+
CHILD_REQUIRES_ACTION_MAX_REQUESTS,
|
|
57
|
+
CHILD_REQUIRES_ACTION_PAYLOAD_MAX_BYTES,
|
|
58
|
+
CHILD_REQUIRES_ACTION_QUESTION_PREVIEW_MAX_BYTES,
|
|
59
|
+
approvalIdentifier
|
|
60
|
+
} from "@opengeni/contracts";
|
|
61
|
+
var childLifecycleNoticesEnabledFlag = false;
|
|
62
|
+
function configureChildLifecycleNotices(input) {
|
|
63
|
+
childLifecycleNoticesEnabledFlag = input.enabled === true;
|
|
64
|
+
}
|
|
65
|
+
function childLifecycleNoticesEnabled() {
|
|
66
|
+
return childLifecycleNoticesEnabledFlag;
|
|
67
|
+
}
|
|
68
|
+
function boundedChildNoticeText(value, maxBytes) {
|
|
69
|
+
const encoder = new TextEncoder();
|
|
70
|
+
const bytes = encoder.encode(value);
|
|
71
|
+
if (bytes.byteLength <= maxBytes) return { text: value, truncated: false };
|
|
72
|
+
const marker = "\u2026";
|
|
73
|
+
const budget = Math.max(0, maxBytes - encoder.encode(marker).byteLength);
|
|
74
|
+
let end = Math.min(budget, bytes.byteLength);
|
|
75
|
+
while (end > 0 && end < bytes.byteLength && (bytes[end] & 192) === 128) end -= 1;
|
|
76
|
+
return { text: `${new TextDecoder().decode(bytes.subarray(0, end))}${marker}`, truncated: true };
|
|
77
|
+
}
|
|
78
|
+
var CHILD_LIFECYCLE_DEDUPE_PREFIX = {
|
|
79
|
+
requiresAction: "child-requires-action",
|
|
80
|
+
requiresActionResolved: "child-requires-action-resolved",
|
|
81
|
+
paused: "child-paused",
|
|
82
|
+
waitingCapacity: "child-waiting-capacity",
|
|
83
|
+
progress: "child-progress"
|
|
84
|
+
};
|
|
85
|
+
function childRequiresActionDedupeKey(input) {
|
|
86
|
+
return `${CHILD_LIFECYCLE_DEDUPE_PREFIX.requiresAction}:${input.childSessionId}:${input.turnId}:${input.turnGeneration}`;
|
|
87
|
+
}
|
|
88
|
+
function childRequiresActionResolvedDedupeKey(input) {
|
|
89
|
+
return `${CHILD_LIFECYCLE_DEDUPE_PREFIX.requiresActionResolved}:${input.childSessionId}:${input.turnId}:${input.turnGeneration}:${input.requestId ?? input.approvalId ?? "unknown"}`;
|
|
90
|
+
}
|
|
91
|
+
function childPausedDedupeKey(input) {
|
|
92
|
+
return `${CHILD_LIFECYCLE_DEDUPE_PREFIX.paused}:${input.childSessionId}:${input.receiptId}`;
|
|
93
|
+
}
|
|
94
|
+
function childWaitingCapacityDedupeKey(input) {
|
|
95
|
+
return `${CHILD_LIFECYCLE_DEDUPE_PREFIX.waitingCapacity}:${input.childSessionId}:${input.waiterId}`;
|
|
96
|
+
}
|
|
97
|
+
function childProgressDedupeKey(input) {
|
|
98
|
+
return `${CHILD_LIFECYCLE_DEDUPE_PREFIX.progress}:${input.childSessionId}:${input.receiptId}`;
|
|
99
|
+
}
|
|
100
|
+
function approvalToolName(value) {
|
|
101
|
+
if (!value || typeof value !== "object") return null;
|
|
102
|
+
const approval = value;
|
|
103
|
+
const rawItem = approval.rawItem && typeof approval.rawItem === "object" ? approval.rawItem : null;
|
|
104
|
+
const candidate = rawItem?.name ?? approval.name;
|
|
105
|
+
return typeof candidate === "string" && candidate.length > 0 ? boundedChildNoticeText(candidate, 128).text : null;
|
|
106
|
+
}
|
|
107
|
+
function buildChildRequiresActionPayload(input) {
|
|
108
|
+
const requests = [];
|
|
109
|
+
for (const request of input.humanInputRequests) {
|
|
110
|
+
const first = request.questions[0];
|
|
111
|
+
requests.push({
|
|
112
|
+
kind: "human_input",
|
|
113
|
+
requestId: request.id,
|
|
114
|
+
questionCount: request.questions.length,
|
|
115
|
+
firstQuestion: boundedChildNoticeText(
|
|
116
|
+
typeof first?.prompt === "string" ? first.prompt : "",
|
|
117
|
+
CHILD_REQUIRES_ACTION_QUESTION_PREVIEW_MAX_BYTES
|
|
118
|
+
).text,
|
|
119
|
+
allowSkip: request.allowSkip,
|
|
120
|
+
expiresAt: request.expiresAt ? request.expiresAt.toISOString() : null
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const seenApprovals = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const pending of input.pendingApprovals) {
|
|
125
|
+
const approvalId = approvalIdentifier(pending);
|
|
126
|
+
if (!approvalId || seenApprovals.has(approvalId)) continue;
|
|
127
|
+
seenApprovals.add(approvalId);
|
|
128
|
+
requests.push({
|
|
129
|
+
kind: "approval",
|
|
130
|
+
approvalId: boundedChildNoticeText(approvalId, 256).text,
|
|
131
|
+
toolName: approvalToolName(pending)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
let truncated = requests.length > CHILD_REQUIRES_ACTION_MAX_REQUESTS;
|
|
135
|
+
let bounded = requests.slice(0, CHILD_REQUIRES_ACTION_MAX_REQUESTS);
|
|
136
|
+
const build = () => ({
|
|
137
|
+
type: "child_requires_action",
|
|
138
|
+
childSessionId: input.childSessionId,
|
|
139
|
+
childTurnId: input.childTurnId,
|
|
140
|
+
childTurnGeneration: input.childTurnGeneration,
|
|
141
|
+
requests: bounded,
|
|
142
|
+
truncated
|
|
143
|
+
});
|
|
144
|
+
let payload = build();
|
|
145
|
+
while (bounded.length > 0 && Buffer.byteLength(JSON.stringify(payload)) > CHILD_REQUIRES_ACTION_PAYLOAD_MAX_BYTES) {
|
|
146
|
+
bounded = bounded.slice(0, -1);
|
|
147
|
+
truncated = true;
|
|
148
|
+
payload = build();
|
|
149
|
+
}
|
|
150
|
+
return payload;
|
|
151
|
+
}
|
|
152
|
+
function childRequiresActionSummary(childSessionId, payload) {
|
|
153
|
+
const humanInputs = payload.requests.filter((request) => request.kind === "human_input");
|
|
154
|
+
const approvals = payload.requests.filter((request) => request.kind === "approval");
|
|
155
|
+
const first = humanInputs[0];
|
|
156
|
+
const lines = [
|
|
157
|
+
`Worker ${childSessionId} is blocked and needs input (turn ${payload.childTurnId}).`
|
|
158
|
+
];
|
|
159
|
+
if (first && first.kind === "human_input") {
|
|
160
|
+
lines.push(
|
|
161
|
+
`It asked: ${first.firstQuestion}${first.questionCount > 1 ? ` (+${first.questionCount - 1} more question${first.questionCount > 2 ? "s" : ""})` : ""}${humanInputs.length > 1 ? ` [${humanInputs.length} open requests]` : ""}.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (approvals.length > 0) {
|
|
165
|
+
const names = approvals.map((request) => request.kind === "approval" ? request.toolName : null).filter((name) => name !== null);
|
|
166
|
+
lines.push(
|
|
167
|
+
`${approvals.length} tool approval${approvals.length > 1 ? "s are" : " is"} waiting for a human${names.length > 0 ? ` (${names.join(", ")})` : ""}.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (payload.truncated) lines.push("The request list was truncated.");
|
|
171
|
+
return lines.join(" ");
|
|
172
|
+
}
|
|
173
|
+
function childRequiresActionResolvedSummary(childSessionId, payload) {
|
|
174
|
+
const subject = payload.requestId ? "input request" : "approval";
|
|
175
|
+
return `Worker ${childSessionId}: its ${subject} was ${payload.outcome} (by ${payload.respondedByKind.replace("_", " ")}); the worker is no longer blocked on it.`;
|
|
176
|
+
}
|
|
177
|
+
function childPausedSummary(childSessionId, payload) {
|
|
178
|
+
const base = `Worker ${childSessionId} was paused by ${payload.actorKind === "agent" ? "an agent" : payload.actorKind === "api" ? "the API" : "a human"}.`;
|
|
179
|
+
return payload.reason ? `${base} Reason: ${payload.reason}` : base;
|
|
180
|
+
}
|
|
181
|
+
function childWaitingCapacitySummary(childSessionId, payload) {
|
|
182
|
+
return `Worker ${childSessionId} is waiting for ${payload.provider === "codex" ? "Codex" : "xAI"} provider capacity${payload.nextCheckAt ? ` (next check ${payload.nextCheckAt})` : ""}; its turn resumes automatically.`;
|
|
183
|
+
}
|
|
184
|
+
function childProgressSummary(childSessionId, payload) {
|
|
185
|
+
return `Worker ${childSessionId} progress: ${payload.progressNote}`;
|
|
186
|
+
}
|
|
187
|
+
function boundedChildPausedReason(reason) {
|
|
188
|
+
if (reason === null || reason === void 0 || reason.length === 0) return null;
|
|
189
|
+
return boundedChildNoticeText(reason, CHILD_PAUSED_REASON_MAX_BYTES).text;
|
|
190
|
+
}
|
|
191
|
+
function boundedChildProgressNote(note) {
|
|
192
|
+
return boundedChildNoticeText(note, CHILD_PROGRESS_NOTE_MAX_BYTES).text;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/session-tool-call-settlement.ts
|
|
196
|
+
import { canonicalizePersistedHistoryItem, omitOutputOnlyHistoryItemFields } from "@opengeni/codex";
|
|
197
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
198
|
+
var TOOL_RESULT_TYPE_BY_CALL_TYPE = {
|
|
199
|
+
function_call: "function_call_result",
|
|
200
|
+
computer_call: "computer_call_result",
|
|
201
|
+
shell_call: "shell_call_output",
|
|
202
|
+
apply_patch_call: "apply_patch_call_output",
|
|
203
|
+
tool_search_call: "tool_search_output"
|
|
204
|
+
};
|
|
205
|
+
function historyCallId(item) {
|
|
206
|
+
const providerData = item.providerData && typeof item.providerData === "object" && !Array.isArray(item.providerData) ? item.providerData : null;
|
|
207
|
+
const value = [
|
|
208
|
+
item.callId,
|
|
209
|
+
item.call_id,
|
|
210
|
+
providerData?.callId,
|
|
211
|
+
providerData?.call_id,
|
|
212
|
+
item.id
|
|
213
|
+
].find((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
214
|
+
return value ?? null;
|
|
215
|
+
}
|
|
216
|
+
function historyItemType(item) {
|
|
217
|
+
return typeof item.type === "string" ? item.type : null;
|
|
218
|
+
}
|
|
219
|
+
function interruptedToolCallResult(input) {
|
|
220
|
+
const message = `Tool execution was interrupted by ${input.reason} before its result was durably recorded. The side-effect outcome is unknown; inspect actual state before repeating the call.`;
|
|
221
|
+
if (input.callType === "function_call") {
|
|
222
|
+
const name = typeof input.callItem.name === "string" ? input.callItem.name : "tool";
|
|
223
|
+
return {
|
|
224
|
+
type: "function_call_result",
|
|
225
|
+
name,
|
|
226
|
+
...typeof input.callItem.namespace === "string" ? { namespace: input.callItem.namespace } : {},
|
|
227
|
+
callId: input.callId,
|
|
228
|
+
status: "incomplete",
|
|
229
|
+
output: { type: "text", text: message }
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (input.callType === "shell_call") {
|
|
233
|
+
return {
|
|
234
|
+
type: "shell_call_output",
|
|
235
|
+
callId: input.callId,
|
|
236
|
+
output: [{ stdout: "", stderr: message, outcome: { type: "exit", exitCode: null } }]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (input.callType === "apply_patch_call") {
|
|
240
|
+
return {
|
|
241
|
+
type: "apply_patch_call_output",
|
|
242
|
+
callId: input.callId,
|
|
243
|
+
status: "failed",
|
|
244
|
+
output: message
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (input.callType === "computer_call") return null;
|
|
248
|
+
if (input.callType === "tool_search_call") {
|
|
249
|
+
const providerData = input.callItem.providerData && typeof input.callItem.providerData === "object" && !Array.isArray(input.callItem.providerData) ? input.callItem.providerData : null;
|
|
250
|
+
const execution = input.callItem.execution === "client" || input.callItem.execution === "server" ? input.callItem.execution : providerData?.execution === "client" || providerData?.execution === "server" ? providerData.execution : null;
|
|
251
|
+
const providerIdentity = typeof providerData?.call_id === "string" || typeof providerData?.callId === "string";
|
|
252
|
+
const snakeId = typeof input.callItem.call_id === "string";
|
|
253
|
+
return {
|
|
254
|
+
type: "tool_search_output",
|
|
255
|
+
...providerIdentity ? {
|
|
256
|
+
providerData: {
|
|
257
|
+
call_id: input.callId,
|
|
258
|
+
...execution ? { execution } : {}
|
|
259
|
+
}
|
|
260
|
+
} : snakeId ? { call_id: input.callId, ...execution ? { execution } : {} } : { callId: input.callId, ...execution ? { execution } : {} },
|
|
261
|
+
status: "incomplete",
|
|
262
|
+
tools: []
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function mapEvent(row) {
|
|
268
|
+
return {
|
|
269
|
+
id: row.id,
|
|
270
|
+
workspaceId: row.workspaceId,
|
|
271
|
+
sessionId: row.sessionId,
|
|
272
|
+
sequence: row.sequence,
|
|
273
|
+
type: row.type,
|
|
274
|
+
payload: fromPostgresLosslessJson(row.payload, row.payloadCodecVersion),
|
|
275
|
+
occurredAt: row.occurredAt.toISOString(),
|
|
276
|
+
clientEventId: row.clientEventId,
|
|
277
|
+
turnId: row.turnId,
|
|
278
|
+
turnGeneration: row.turnGeneration,
|
|
279
|
+
turnAttemptId: row.turnAttemptId,
|
|
280
|
+
turnAssociation: row.turnAssociation,
|
|
281
|
+
duplicateOfEventId: row.duplicateOfEventId,
|
|
282
|
+
duplicateReason: row.duplicateReason
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function closePendingSessionToolCallsInTransaction(tx, input) {
|
|
286
|
+
const pendingRows = await tx.select().from(sessionPendingToolCalls).where(
|
|
287
|
+
and(
|
|
288
|
+
eq(sessionPendingToolCalls.workspaceId, input.workspaceId),
|
|
289
|
+
eq(sessionPendingToolCalls.sessionId, input.sessionId),
|
|
290
|
+
eq(sessionPendingToolCalls.turnId, input.turnId)
|
|
291
|
+
)
|
|
292
|
+
).orderBy(asc(sessionPendingToolCalls.createdAt), asc(sessionPendingToolCalls.id)).for("update");
|
|
293
|
+
const pending = pendingRows.filter((row) => !(input.preserveInterruptionRows && row.interruptionKind !== null)).map((row) => ({
|
|
294
|
+
...row,
|
|
295
|
+
callItem: fromPostgresLosslessJson(row.callItem, row.callItemCodecVersion),
|
|
296
|
+
resultItem: row.resultItem === null ? null : fromPostgresLosslessJson(row.resultItem, row.resultItemCodecVersion),
|
|
297
|
+
eventOutput: row.eventOutput === null ? null : fromPostgresLosslessJson(row.eventOutput, row.eventOutputCodecVersion)
|
|
298
|
+
}));
|
|
299
|
+
if (pending.length === 0) return { sequence: input.sequence, events: [], closed: 0 };
|
|
300
|
+
const history = await tx.select({
|
|
301
|
+
position: sessionHistoryItems.position,
|
|
302
|
+
item: sessionHistoryItems.item,
|
|
303
|
+
itemCodecVersion: sessionHistoryItems.itemCodecVersion,
|
|
304
|
+
active: sessionHistoryItems.active
|
|
305
|
+
}).from(sessionHistoryItems).where(
|
|
306
|
+
and(
|
|
307
|
+
eq(sessionHistoryItems.workspaceId, input.workspaceId),
|
|
308
|
+
eq(sessionHistoryItems.sessionId, input.sessionId),
|
|
309
|
+
eq(sessionHistoryItems.turnId, input.turnId)
|
|
310
|
+
)
|
|
311
|
+
).orderBy(asc(sessionHistoryItems.position));
|
|
312
|
+
const decodedHistory = history.map((row) => ({
|
|
313
|
+
...row,
|
|
314
|
+
item: fromPostgresLosslessJson(row.item, row.itemCodecVersion)
|
|
315
|
+
}));
|
|
316
|
+
const [{ maxPosition } = { maxPosition: -1 }] = await tx.select({ maxPosition: sql`coalesce(max(${sessionHistoryItems.position}), -1)` }).from(sessionHistoryItems).where(
|
|
317
|
+
and(
|
|
318
|
+
eq(sessionHistoryItems.workspaceId, input.workspaceId),
|
|
319
|
+
eq(sessionHistoryItems.sessionId, input.sessionId)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
const existingOutputEvents = await tx.select({ callId: sql`${sessionEvents.payload} ->> 'id'` }).from(sessionEvents).where(
|
|
323
|
+
and(
|
|
324
|
+
eq(sessionEvents.workspaceId, input.workspaceId),
|
|
325
|
+
eq(sessionEvents.sessionId, input.sessionId),
|
|
326
|
+
eq(sessionEvents.turnId, input.turnId),
|
|
327
|
+
eq(sessionEvents.type, "agent.toolCall.output")
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
const projectedCallIds = new Set(
|
|
331
|
+
existingOutputEvents.flatMap(({ callId }) => callId ? [callId] : [])
|
|
332
|
+
);
|
|
333
|
+
let nextPosition = Math.floor(Number(maxPosition)) + 1;
|
|
334
|
+
let sequence = input.sequence;
|
|
335
|
+
const historyValues = [];
|
|
336
|
+
const eventValues = [];
|
|
337
|
+
const resolutions = pending.map((call) => {
|
|
338
|
+
const resultType = TOOL_RESULT_TYPE_BY_CALL_TYPE[call.callType];
|
|
339
|
+
const existingCall = decodedHistory.find(
|
|
340
|
+
({ item }) => historyItemType(item) === call.callType && historyCallId(item) === call.callId
|
|
341
|
+
);
|
|
342
|
+
const existingResult = resultType ? decodedHistory.find(
|
|
343
|
+
({ item, position }) => position > (existingCall?.position ?? Number.MAX_SAFE_INTEGER) && historyItemType(item) === resultType && historyCallId(item) === call.callId
|
|
344
|
+
) : void 0;
|
|
345
|
+
const activeCall = decodedHistory.find(
|
|
346
|
+
({ item, active }) => active && historyItemType(item) === call.callType && historyCallId(item) === call.callId
|
|
347
|
+
);
|
|
348
|
+
const activeResult = resultType ? decodedHistory.find(
|
|
349
|
+
({ item, active, position }) => active && position > (activeCall?.position ?? Number.MAX_SAFE_INTEGER) && historyItemType(item) === resultType && historyCallId(item) === call.callId
|
|
350
|
+
) : void 0;
|
|
351
|
+
const interruptedResult = interruptedToolCallResult({
|
|
352
|
+
callType: call.callType,
|
|
353
|
+
callId: call.callId,
|
|
354
|
+
callItem: call.callItem,
|
|
355
|
+
reason: input.reason
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
call,
|
|
359
|
+
existingCall,
|
|
360
|
+
existingResult,
|
|
361
|
+
activeCall,
|
|
362
|
+
activeResult,
|
|
363
|
+
completeDurablePair: Boolean(existingCall && existingResult),
|
|
364
|
+
supersededDurablePair: Boolean(
|
|
365
|
+
existingCall && existingResult && (!existingCall.active || !existingResult.active)
|
|
366
|
+
),
|
|
367
|
+
rawCallIsValid: historyItemType(call.callItem) === call.callType,
|
|
368
|
+
result: existingResult?.item ?? call.resultItem ?? interruptedResult,
|
|
369
|
+
interrupted: !existingResult && !call.resultItem
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
for (const resolution of resolutions) {
|
|
373
|
+
if (!resolution.completeDurablePair && !resolution.activeCall && resolution.result && resolution.rawCallIsValid) {
|
|
374
|
+
historyValues.push({
|
|
375
|
+
accountId: input.accountId,
|
|
376
|
+
workspaceId: input.workspaceId,
|
|
377
|
+
sessionId: input.sessionId,
|
|
378
|
+
turnId: input.turnId,
|
|
379
|
+
position: nextPosition++,
|
|
380
|
+
item: omitOutputOnlyHistoryItemFields(resolution.call.callItem),
|
|
381
|
+
itemCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
382
|
+
active: true
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const orderedResults = [...resolutions].sort(
|
|
387
|
+
(left, right) => (left.call.resultRecordedAt?.getTime() ?? Number.MAX_SAFE_INTEGER) - (right.call.resultRecordedAt?.getTime() ?? Number.MAX_SAFE_INTEGER)
|
|
388
|
+
);
|
|
389
|
+
for (const resolution of orderedResults) {
|
|
390
|
+
if (!resolution.completeDurablePair && !resolution.activeResult && resolution.result && resolution.rawCallIsValid) {
|
|
391
|
+
historyValues.push({
|
|
392
|
+
accountId: input.accountId,
|
|
393
|
+
workspaceId: input.workspaceId,
|
|
394
|
+
sessionId: input.sessionId,
|
|
395
|
+
turnId: input.turnId,
|
|
396
|
+
position: nextPosition++,
|
|
397
|
+
item: canonicalizePersistedHistoryItem(
|
|
398
|
+
resolution.result,
|
|
399
|
+
resolution.call.modelToolOutputTruncationTokens ?? void 0
|
|
400
|
+
),
|
|
401
|
+
itemCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
402
|
+
active: true
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (resolution.supersededDurablePair || projectedCallIds.has(resolution.call.callId)) continue;
|
|
406
|
+
eventValues.push({
|
|
407
|
+
accountId: input.accountId,
|
|
408
|
+
workspaceId: input.workspaceId,
|
|
409
|
+
sessionId: input.sessionId,
|
|
410
|
+
sequence: ++sequence,
|
|
411
|
+
type: "agent.toolCall.output",
|
|
412
|
+
turnId: input.turnId,
|
|
413
|
+
turnGeneration: resolution.call.executionGeneration,
|
|
414
|
+
turnAttemptId: resolution.call.attemptId,
|
|
415
|
+
turnAssociation: input.turnAssociation === void 0 ? "current" : input.turnAssociation,
|
|
416
|
+
payload: {
|
|
417
|
+
id: resolution.call.callId,
|
|
418
|
+
output: resolution.interrupted ? {
|
|
419
|
+
isError: true,
|
|
420
|
+
content: [
|
|
421
|
+
{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: `Tool execution was interrupted by ${input.reason}; its side-effect outcome is unknown.`
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
} : resolution.call.eventOutput !== null ? resolution.call.eventOutput.value : (resolution.existingResult?.item ?? resolution.call.resultItem)?.output ?? resolution.existingResult?.item ?? resolution.call.resultItem,
|
|
427
|
+
recovery: {
|
|
428
|
+
interrupted: resolution.interrupted,
|
|
429
|
+
outcome: resolution.interrupted ? "unknown" : "durable_result_found",
|
|
430
|
+
reason: input.reason,
|
|
431
|
+
unsupportedCallShape: resolution.interrupted && (!resolution.result || !resolution.rawCallIsValid)
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
payloadCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
435
|
+
occurredAt: input.now
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (historyValues.length > 0) await tx.insert(sessionHistoryItems).values(historyValues);
|
|
439
|
+
const inserted = eventValues.length > 0 ? await tx.insert(sessionEvents).values(eventValues).returning() : [];
|
|
440
|
+
await cancelUnacceptedVideoGenerationsForToolCallsInTransaction(tx, {
|
|
441
|
+
workspaceId: input.workspaceId,
|
|
442
|
+
sessionId: input.sessionId,
|
|
443
|
+
turnId: input.turnId,
|
|
444
|
+
toolCallIds: pending.map((call) => call.callId),
|
|
445
|
+
reason: input.reason,
|
|
446
|
+
now: input.now
|
|
447
|
+
});
|
|
448
|
+
await tx.delete(sessionPendingToolCalls).where(
|
|
449
|
+
and(
|
|
450
|
+
eq(sessionPendingToolCalls.workspaceId, input.workspaceId),
|
|
451
|
+
eq(sessionPendingToolCalls.sessionId, input.sessionId),
|
|
452
|
+
eq(sessionPendingToolCalls.turnId, input.turnId),
|
|
453
|
+
inArray(
|
|
454
|
+
sessionPendingToolCalls.id,
|
|
455
|
+
pending.map((call) => call.id)
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
);
|
|
459
|
+
return { sequence, events: inserted.map(mapEvent), closed: pending.length };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/session-realtime-mirror.ts
|
|
463
|
+
import { createHash } from "crypto";
|
|
464
|
+
import { and as and2, desc, eq as eq2, gt, sql as sql2 } from "drizzle-orm";
|
|
465
|
+
var SESSION_REALTIME_MIRROR_MAX_TEXT_BYTES = 131072;
|
|
466
|
+
var SESSION_REALTIME_MIRROR_MAX_PAYLOAD_BYTES = 131072;
|
|
467
|
+
var TRUNCATION_MARKER = "\n\u2026realtime context truncated\u2026";
|
|
468
|
+
function deterministicUuid(seed) {
|
|
469
|
+
const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
470
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
471
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
472
|
+
const hex = bytes.toString("hex");
|
|
473
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
474
|
+
}
|
|
475
|
+
function takeUtf8Head(value, maximumBytes) {
|
|
476
|
+
const bytes = Buffer.from(value, "utf8");
|
|
477
|
+
if (bytes.length <= maximumBytes) return value;
|
|
478
|
+
let end = maximumBytes;
|
|
479
|
+
while (end > 0 && (bytes[end] & 192) === 128) end -= 1;
|
|
480
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
481
|
+
}
|
|
482
|
+
function boundedText(value) {
|
|
483
|
+
if (Buffer.byteLength(value, "utf8") <= SESSION_REALTIME_MIRROR_MAX_TEXT_BYTES) return value;
|
|
484
|
+
const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
|
|
485
|
+
return `${takeUtf8Head(value, SESSION_REALTIME_MIRROR_MAX_TEXT_BYTES - markerBytes)}${TRUNCATION_MARKER}`;
|
|
486
|
+
}
|
|
487
|
+
function boundedPayload(value) {
|
|
488
|
+
const payload = value;
|
|
489
|
+
if (Buffer.byteLength(JSON.stringify(payload), "utf8") > SESSION_REALTIME_MIRROR_MAX_PAYLOAD_BYTES) {
|
|
490
|
+
throw new Error("Realtime mirror payload exceeds the durable ledger limit");
|
|
491
|
+
}
|
|
492
|
+
return payload;
|
|
493
|
+
}
|
|
494
|
+
async function mirrorSessionRealtimeContextInTransaction(db, input) {
|
|
495
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
496
|
+
const modes = await db.select().from(sessionRealtimeModes).where(
|
|
497
|
+
and2(
|
|
498
|
+
eq2(sessionRealtimeModes.accountId, input.accountId),
|
|
499
|
+
eq2(sessionRealtimeModes.workspaceId, input.workspaceId),
|
|
500
|
+
eq2(sessionRealtimeModes.sessionId, input.sessionId),
|
|
501
|
+
eq2(sessionRealtimeModes.state, "active"),
|
|
502
|
+
gt(sessionRealtimeModes.leaseExpiresAt, now)
|
|
503
|
+
)
|
|
504
|
+
).orderBy(desc(sessionRealtimeModes.startedAt)).for("update").limit(2);
|
|
505
|
+
if (modes.length === 0) return null;
|
|
506
|
+
if (modes.length !== 1) {
|
|
507
|
+
throw new Error(`Session ${input.sessionId} has multiple active realtime modes`);
|
|
508
|
+
}
|
|
509
|
+
const mode = modes[0];
|
|
510
|
+
const operationId = deterministicUuid(
|
|
511
|
+
`opengeni:session-realtime-mirror:${mode.id}:${input.sourceKind}:${input.sourceId}`
|
|
512
|
+
);
|
|
513
|
+
const [existing] = await db.select().from(sessionRealtimeEntries).where(
|
|
514
|
+
and2(
|
|
515
|
+
eq2(sessionRealtimeEntries.realtimeId, mode.id),
|
|
516
|
+
eq2(sessionRealtimeEntries.operationId, operationId)
|
|
517
|
+
)
|
|
518
|
+
).limit(1);
|
|
519
|
+
if (existing) return { entry: existing, replay: true };
|
|
520
|
+
const [sequenceRow] = await db.select({ next: sql2`coalesce(max(${sessionRealtimeEntries.sequence}), 0) + 1` }).from(sessionRealtimeEntries).where(eq2(sessionRealtimeEntries.realtimeId, mode.id));
|
|
521
|
+
const payload = boundedPayload({
|
|
522
|
+
...input.payload ?? {},
|
|
523
|
+
source: input.sourceKind,
|
|
524
|
+
sourceId: input.sourceId,
|
|
525
|
+
channel: input.channel,
|
|
526
|
+
...input.turnId ? { sourceTurnId: input.turnId } : {}
|
|
527
|
+
});
|
|
528
|
+
const [entry] = await db.insert(sessionRealtimeEntries).values({
|
|
529
|
+
accountId: input.accountId,
|
|
530
|
+
workspaceId: input.workspaceId,
|
|
531
|
+
sessionId: input.sessionId,
|
|
532
|
+
realtimeId: mode.id,
|
|
533
|
+
operationId,
|
|
534
|
+
connectionEpoch: mode.connectionEpoch,
|
|
535
|
+
sequence: Number(sequenceRow?.next ?? 1),
|
|
536
|
+
direction: "provider_out",
|
|
537
|
+
kind: "session_update",
|
|
538
|
+
text: boundedText(input.text),
|
|
539
|
+
textCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
540
|
+
payload,
|
|
541
|
+
payloadCodecVersion: LOSSLESS_CONTENT_CODEC_VERSION,
|
|
542
|
+
createdAt: now,
|
|
543
|
+
updatedAt: now
|
|
544
|
+
}).returning();
|
|
545
|
+
if (!entry) throw new Error("Failed to append realtime session context");
|
|
546
|
+
return { entry, replay: false };
|
|
547
|
+
}
|
|
548
|
+
function renderRealtimeHumanInputContext(input) {
|
|
549
|
+
return [
|
|
550
|
+
"<session_user_message>",
|
|
551
|
+
` <status>${input.routing}</status>`,
|
|
552
|
+
` <delivery>${input.delivery}</delivery>`,
|
|
553
|
+
` <text>${escapeXmlText(input.text)}</text>`,
|
|
554
|
+
" <instruction>Already routed by OpenGeni; do not delegate this message again.</instruction>",
|
|
555
|
+
"</session_user_message>"
|
|
556
|
+
].join("\n");
|
|
557
|
+
}
|
|
558
|
+
function renderRealtimeHumanInputRequestContext(input) {
|
|
559
|
+
const lines = ["<session_human_input_request>", " <status>waiting_for_user</status>"];
|
|
560
|
+
for (const request of input.requests) {
|
|
561
|
+
lines.push(
|
|
562
|
+
" <request>",
|
|
563
|
+
` <id>${escapeXmlText(request.id)}</id>`,
|
|
564
|
+
` <allow_skip>${String(request.allowSkip)}</allow_skip>`,
|
|
565
|
+
` <expires_at>${escapeXmlText(renderExpiry(request.expiresAt))}</expires_at>`
|
|
566
|
+
);
|
|
567
|
+
for (const question of request.questions) {
|
|
568
|
+
lines.push(
|
|
569
|
+
" <question>",
|
|
570
|
+
` <id>${escapeXmlText(question.id)}</id>`,
|
|
571
|
+
` <kind>${escapeXmlText(question.kind)}</kind>`,
|
|
572
|
+
` <required>${String(question.required)}</required>`
|
|
573
|
+
);
|
|
574
|
+
if (question.label) lines.push(` <label>${escapeXmlText(question.label)}</label>`);
|
|
575
|
+
lines.push(` <prompt>${escapeXmlText(question.prompt)}</prompt>`);
|
|
576
|
+
if (question.helpText) {
|
|
577
|
+
lines.push(` <help_text>${escapeXmlText(question.helpText)}</help_text>`);
|
|
578
|
+
}
|
|
579
|
+
if (question.options.length > 0) {
|
|
580
|
+
lines.push(" <options>");
|
|
581
|
+
for (const option of question.options) {
|
|
582
|
+
lines.push(
|
|
583
|
+
" <option>",
|
|
584
|
+
` <id>${escapeXmlText(option.id)}</id>`,
|
|
585
|
+
` <label>${escapeXmlText(option.label)}</label>`
|
|
586
|
+
);
|
|
587
|
+
if (option.description) {
|
|
588
|
+
lines.push(` <description>${escapeXmlText(option.description)}</description>`);
|
|
589
|
+
}
|
|
590
|
+
lines.push(" </option>");
|
|
591
|
+
}
|
|
592
|
+
lines.push(" </options>");
|
|
593
|
+
}
|
|
594
|
+
lines.push(
|
|
595
|
+
` <allow_other>${String(question.kind !== "text")}</allow_other>`,
|
|
596
|
+
" </question>"
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
lines.push(" </request>");
|
|
600
|
+
}
|
|
601
|
+
lines.push(
|
|
602
|
+
" <instruction>The current work is waiting for the user's input. Ask the questions naturally, one at a time when useful, without changing their meaning or answering for the user. The user may answer in the visible form or answer conversationally here. If the user answers here, delegate exactly once with a complete message containing the relevant question and the user's answer. If the user changes direction instead, delegate that new direction normally. Do not claim the work resumed until a later session update confirms it.</instruction>",
|
|
603
|
+
"</session_human_input_request>"
|
|
604
|
+
);
|
|
605
|
+
return lines.join("\n");
|
|
606
|
+
}
|
|
607
|
+
function renderRealtimeHumanInputResponseContext(input) {
|
|
608
|
+
const lines = [
|
|
609
|
+
"<session_human_input_response>",
|
|
610
|
+
` <request_id>${escapeXmlText(input.requestId)}</request_id>`,
|
|
611
|
+
` <outcome>${escapeXmlText(input.response.outcome)}</outcome>`
|
|
612
|
+
];
|
|
613
|
+
if (input.response.outcome === "answered") {
|
|
614
|
+
const questions = new Map(input.questions.map((question) => [question.id, question]));
|
|
615
|
+
lines.push(" <answers>");
|
|
616
|
+
for (const answer of input.response.answers) {
|
|
617
|
+
const question = questions.get(answer.questionId);
|
|
618
|
+
const optionLabels = new Map(
|
|
619
|
+
question?.options.map((option) => [option.id, option.label]) ?? []
|
|
620
|
+
);
|
|
621
|
+
lines.push(
|
|
622
|
+
" <answer>",
|
|
623
|
+
` <question_id>${escapeXmlText(answer.questionId)}</question_id>`
|
|
624
|
+
);
|
|
625
|
+
if (question) lines.push(` <question>${escapeXmlText(question.prompt)}</question>`);
|
|
626
|
+
for (const value of answer.values) {
|
|
627
|
+
lines.push(` <value>${escapeXmlText(optionLabels.get(value) ?? value)}</value>`);
|
|
628
|
+
}
|
|
629
|
+
if (answer.other) lines.push(` <other>${escapeXmlText(answer.other)}</other>`);
|
|
630
|
+
lines.push(" </answer>");
|
|
631
|
+
}
|
|
632
|
+
lines.push(" </answers>");
|
|
633
|
+
}
|
|
634
|
+
lines.push(
|
|
635
|
+
input.response.outcome === "answered" || input.response.outcome === "skipped" ? " <instruction>This structured response was accepted through the session UI and the same work can resume. Treat it as authoritative user context, do not delegate it again, and acknowledge briefly only if useful.</instruction>" : " <instruction>This pending question is no longer active. Do not ask it again unless the user raises it.</instruction>",
|
|
636
|
+
"</session_human_input_response>"
|
|
637
|
+
);
|
|
638
|
+
return lines.join("\n");
|
|
639
|
+
}
|
|
640
|
+
function renderExpiry(value) {
|
|
641
|
+
if (value === null || value === void 0) return "none";
|
|
642
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
643
|
+
}
|
|
644
|
+
function escapeXmlText(value) {
|
|
645
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/session-control.ts
|
|
649
|
+
var SESSION_ANCESTRY_LIMIT = 1e4;
|
|
650
|
+
var NO_SETTLEMENT_ATTEMPTS = {
|
|
651
|
+
attemptCount: 0,
|
|
652
|
+
interruptionPendingCount: 0,
|
|
653
|
+
quiescencePendingCount: 0
|
|
654
|
+
};
|
|
655
|
+
var SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS = 200;
|
|
656
|
+
var SESSION_DISCOVERY_CONTROL_TARGET_LIMIT = 100;
|
|
657
|
+
function serializeEffectiveSessionControl(control) {
|
|
658
|
+
const blocker = (value) => ({
|
|
659
|
+
...value,
|
|
660
|
+
changedAt: value.changedAt?.toISOString() ?? null
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
...control,
|
|
664
|
+
primaryBlocker: control.primaryBlocker ? blocker(control.primaryBlocker) : null,
|
|
665
|
+
blockers: control.blockers.map(blocker),
|
|
666
|
+
resumeOptions: control.resumeOptions.map(({ remainingPrimaryBlocker, ...option }) => ({
|
|
667
|
+
...option,
|
|
668
|
+
...remainingPrimaryBlocker ? { remainingPrimaryBlocker: blocker(remainingPrimaryBlocker) } : {}
|
|
669
|
+
}))
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
var SessionControlInvariantError = class extends Error {
|
|
673
|
+
code = "SESSION_CONTROL_INVARIANT";
|
|
674
|
+
constructor(message) {
|
|
675
|
+
super(message);
|
|
676
|
+
this.name = "SessionControlInvariantError";
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
var SessionControlConflictError = class extends Error {
|
|
680
|
+
code = "CONTROL_CHANGED";
|
|
681
|
+
constructor(message = "The workstream control changed") {
|
|
682
|
+
super(message);
|
|
683
|
+
this.name = "SessionControlConflictError";
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
var SessionCommandIdempotencyError = class extends Error {
|
|
687
|
+
code = "IDEMPOTENCY_KEY_REUSED";
|
|
688
|
+
constructor() {
|
|
689
|
+
super("The operation key was already used with different input");
|
|
690
|
+
this.name = "SessionCommandIdempotencyError";
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
var AgentCommandAuthorityError = class extends Error {
|
|
694
|
+
constructor(code, message) {
|
|
695
|
+
super(message);
|
|
696
|
+
this.code = code;
|
|
697
|
+
this.name = "AgentCommandAuthorityError";
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
async function assertAgentCommandAuthorityInTransaction(db, input) {
|
|
701
|
+
if (["goal", "wait"].includes(input.action) && input.targetSessionId !== input.actor.sessionId) {
|
|
702
|
+
throw new SessionControlInvariantError("An agent self command must target its own session");
|
|
703
|
+
}
|
|
704
|
+
const authorityLocks = await lockSessionEventWriteRows(db, {
|
|
705
|
+
workspaceId: input.workspaceId,
|
|
706
|
+
controlLock: "already_locked",
|
|
707
|
+
workspaceLock: "already_locked",
|
|
708
|
+
sessionIds: [input.actor.sessionId, input.targetSessionId],
|
|
709
|
+
turnIds: [input.actor.turnId],
|
|
710
|
+
attemptIds: [input.actor.attemptId]
|
|
711
|
+
});
|
|
712
|
+
const lockedSessions = authorityLocks.sessions;
|
|
713
|
+
const callerSession = lockedSessions.find((row) => row.id === input.actor.sessionId);
|
|
714
|
+
if (!callerSession) {
|
|
715
|
+
throw new AgentCommandAuthorityError("CALLER_STALE", "The calling session no longer exists");
|
|
716
|
+
}
|
|
717
|
+
const targetSession = lockedSessions.find((row) => row.id === input.targetSessionId);
|
|
718
|
+
if (!targetSession) {
|
|
719
|
+
throw new SessionControlInvariantError(`Target session not found: ${input.targetSessionId}`);
|
|
720
|
+
}
|
|
721
|
+
const rows = await db.execute(sql3`
|
|
722
|
+
select
|
|
723
|
+
attempt.id as "attemptId",
|
|
724
|
+
attempt.state as "attemptState",
|
|
725
|
+
attempt.session_id as "attemptSessionId",
|
|
726
|
+
attempt.turn_id as "attemptTurnId",
|
|
727
|
+
attempt.execution_generation as "executionGeneration",
|
|
728
|
+
turn.active_attempt_id as "activeAttemptId",
|
|
729
|
+
turn.status as "turnStatus",
|
|
730
|
+
session.active_turn_id as "activeTurnId",
|
|
731
|
+
exists (
|
|
732
|
+
select 1
|
|
733
|
+
from ${sessionAttemptInterruptions} interruption
|
|
734
|
+
where interruption.workspace_id = ${input.workspaceId}
|
|
735
|
+
and interruption.attempt_id = attempt.id
|
|
736
|
+
and interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
737
|
+
) as interrupted
|
|
738
|
+
from ${sessionTurnAttempts} attempt
|
|
739
|
+
join ${sessionTurns} turn
|
|
740
|
+
on turn.workspace_id = attempt.workspace_id and turn.id = attempt.turn_id
|
|
741
|
+
join ${sessions} session
|
|
742
|
+
on session.workspace_id = attempt.workspace_id and session.id = attempt.session_id
|
|
743
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
744
|
+
and attempt.id = ${input.actor.attemptId}
|
|
745
|
+
`);
|
|
746
|
+
const caller = rows[0];
|
|
747
|
+
if (!caller || !["claimed", "running"].includes(caller.attemptState) || caller.attemptSessionId !== input.actor.sessionId || caller.attemptTurnId !== input.actor.turnId || Number(caller.executionGeneration) !== input.actor.executionGeneration || caller.activeAttemptId !== input.actor.attemptId || caller.activeTurnId !== input.actor.turnId || !["running", "requires_action", "recovering", "waiting_capacity"].includes(caller.turnStatus)) {
|
|
748
|
+
throw new AgentCommandAuthorityError(
|
|
749
|
+
"CALLER_STALE",
|
|
750
|
+
"The calling agent attempt no longer owns its turn"
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (caller.interrupted) {
|
|
754
|
+
throw new AgentCommandAuthorityError(
|
|
755
|
+
"CALLER_INTERRUPTED",
|
|
756
|
+
"The calling agent attempt is being interrupted"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (input.action === "steer" && input.targetSessionId === input.actor.sessionId) {
|
|
760
|
+
throw new AgentCommandAuthorityError("SELF_STEER", "An agent cannot steer its own session");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function asSafeRevision(value, label) {
|
|
764
|
+
if (value === null) return null;
|
|
765
|
+
const revision = Number(value);
|
|
766
|
+
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
767
|
+
throw new SessionControlInvariantError(`${label} is not a safe non-negative revision`);
|
|
768
|
+
}
|
|
769
|
+
return revision;
|
|
770
|
+
}
|
|
771
|
+
function asDate(value) {
|
|
772
|
+
if (value === null) return null;
|
|
773
|
+
return value instanceof Date ? value : new Date(value);
|
|
774
|
+
}
|
|
775
|
+
function canonicalize(value) {
|
|
776
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
777
|
+
if (value && typeof value === "object") {
|
|
778
|
+
return Object.fromEntries(
|
|
779
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalize(item)])
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return value;
|
|
783
|
+
}
|
|
784
|
+
function canonicalSessionCommandHash(value) {
|
|
785
|
+
return createHash2("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
|
|
786
|
+
}
|
|
787
|
+
function controlEtag(value) {
|
|
788
|
+
return `sc1:${canonicalSessionCommandHash(value)}`;
|
|
789
|
+
}
|
|
790
|
+
function lockClause(mode) {
|
|
791
|
+
if (mode === "none") return sql3.empty();
|
|
792
|
+
return mode === "update" ? sql3.raw("for update") : sql3.raw("for share");
|
|
793
|
+
}
|
|
794
|
+
var WORKSPACE_CONTROL_REQUEST_LOCK_TIMEOUT_MS = 2e4;
|
|
795
|
+
var configuredRequestLockTimeoutMs = WORKSPACE_CONTROL_REQUEST_LOCK_TIMEOUT_MS;
|
|
796
|
+
function configureWorkspaceControlRequestLockTimeoutMs(lockTimeoutMs) {
|
|
797
|
+
if (!Number.isSafeInteger(lockTimeoutMs) || lockTimeoutMs <= 0) {
|
|
798
|
+
throw new Error("Workspace control request lock timeout must be a positive integer (ms)");
|
|
799
|
+
}
|
|
800
|
+
configuredRequestLockTimeoutMs = lockTimeoutMs;
|
|
801
|
+
}
|
|
802
|
+
function workspaceControlRequestLockTimeoutMs() {
|
|
803
|
+
return configuredRequestLockTimeoutMs;
|
|
804
|
+
}
|
|
805
|
+
var WorkspaceControlBusyError = class extends Error {
|
|
806
|
+
constructor(workspaceId, lockTimeoutMs) {
|
|
807
|
+
super("The workspace control row is busy; retry shortly");
|
|
808
|
+
this.workspaceId = workspaceId;
|
|
809
|
+
this.lockTimeoutMs = lockTimeoutMs;
|
|
810
|
+
this.name = "WorkspaceControlBusyError";
|
|
811
|
+
}
|
|
812
|
+
code = "WORKSPACE_CONTROL_BUSY";
|
|
813
|
+
};
|
|
814
|
+
function workspaceControlLockBudget(lockTimeoutMs) {
|
|
815
|
+
if (!Number.isFinite(lockTimeoutMs) || lockTimeoutMs <= 0) {
|
|
816
|
+
throw new Error("Workspace control lockTimeoutMs must be a positive number");
|
|
817
|
+
}
|
|
818
|
+
const totalMs = Math.max(1, Math.floor(lockTimeoutMs));
|
|
819
|
+
return { totalMs, deadlineAt: Date.now() + totalMs };
|
|
820
|
+
}
|
|
821
|
+
function advisoryLockFunction(mode) {
|
|
822
|
+
return mode === "update" ? sql3.raw("pg_advisory_xact_lock") : sql3.raw("pg_advisory_xact_lock_shared");
|
|
823
|
+
}
|
|
824
|
+
function lockBudgetFor(options) {
|
|
825
|
+
if (options.budget) return options.budget;
|
|
826
|
+
if (options.lockTimeoutMs === void 0) return null;
|
|
827
|
+
return workspaceControlLockBudget(options.lockTimeoutMs);
|
|
828
|
+
}
|
|
829
|
+
async function boundedLockStep(db, workspaceId, budget, step) {
|
|
830
|
+
if (!budget) return await step();
|
|
831
|
+
const remainingMs = budget.deadlineAt - Date.now();
|
|
832
|
+
if (remainingMs <= 0) throw new WorkspaceControlBusyError(workspaceId, budget.totalMs);
|
|
833
|
+
await db.execute(
|
|
834
|
+
sql3`select set_config('lock_timeout', ${`${Math.max(1, Math.floor(remainingMs))}ms`}, true)`
|
|
835
|
+
);
|
|
836
|
+
try {
|
|
837
|
+
return await step();
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (nestedPostgresSqlState(error) === "55P03") {
|
|
840
|
+
throw new WorkspaceControlBusyError(workspaceId, budget.totalMs);
|
|
841
|
+
}
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function lockWorkspaceInferenceControl(db, workspaceId, mode, options = {}) {
|
|
846
|
+
const budget = lockBudgetFor(options);
|
|
847
|
+
const [prior] = budget ? await db.execute(sql3`select current_setting('lock_timeout') as value`) : [];
|
|
848
|
+
if (mode !== "none") {
|
|
849
|
+
await boundedLockStep(
|
|
850
|
+
db,
|
|
851
|
+
workspaceId,
|
|
852
|
+
budget,
|
|
853
|
+
() => db.execute(
|
|
854
|
+
sql3`select ${advisoryLockFunction(mode)}(hashtextextended(${`workspace-control:${workspaceId}`}, 0))`
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const rows = await boundedLockStep(
|
|
859
|
+
db,
|
|
860
|
+
workspaceId,
|
|
861
|
+
budget,
|
|
862
|
+
() => db.execute(sql3`
|
|
863
|
+
select
|
|
864
|
+
workspace_id as "workspaceId",
|
|
865
|
+
account_id as "accountId",
|
|
866
|
+
revision,
|
|
867
|
+
workspace_state as "workspaceState",
|
|
868
|
+
workspace_pause_revision as "workspacePauseRevision",
|
|
869
|
+
reason,
|
|
870
|
+
changed_by as "changedBy",
|
|
871
|
+
changed_at as "changedAt"
|
|
872
|
+
from ${workspaceInferenceControls}
|
|
873
|
+
where workspace_id = ${workspaceId}
|
|
874
|
+
${lockClause(mode)}
|
|
875
|
+
`)
|
|
876
|
+
);
|
|
877
|
+
if (budget) {
|
|
878
|
+
await db.execute(sql3`select set_config('lock_timeout', ${prior?.value ?? "0"}, true)`);
|
|
879
|
+
}
|
|
880
|
+
const row = rows[0];
|
|
881
|
+
if (!row) {
|
|
882
|
+
throw new SessionControlInvariantError(
|
|
883
|
+
`Workspace ${workspaceId} has no mandatory inference-control row`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
return row;
|
|
887
|
+
}
|
|
888
|
+
var WORKSPACE_CONTROL_ADMISSION_SAVEPOINT = "opengeni_workspace_control_admission";
|
|
889
|
+
async function lockWorkspaceInferenceControlForAdmission(db, input) {
|
|
890
|
+
const lockOptions = input.lockTimeoutMs === void 0 ? {} : { budget: workspaceControlLockBudget(input.lockTimeoutMs) };
|
|
891
|
+
await db.execute(sql3.raw(`savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
892
|
+
const shared = await lockWorkspaceInferenceControl(db, input.workspaceId, "share", lockOptions);
|
|
893
|
+
if (!input.resumePausedBranch) {
|
|
894
|
+
await db.execute(sql3.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
895
|
+
return { control: shared, mode: "share" };
|
|
896
|
+
}
|
|
897
|
+
const effective = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
898
|
+
workspaceControl: shared
|
|
899
|
+
});
|
|
900
|
+
if (effective.state === "active") {
|
|
901
|
+
await db.execute(sql3.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
902
|
+
return { control: shared, mode: "share" };
|
|
903
|
+
}
|
|
904
|
+
await db.execute(sql3.raw(`rollback to savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
905
|
+
await db.execute(sql3.raw(`release savepoint ${WORKSPACE_CONTROL_ADMISSION_SAVEPOINT}`));
|
|
906
|
+
return {
|
|
907
|
+
control: await lockWorkspaceInferenceControl(db, input.workspaceId, "update", lockOptions),
|
|
908
|
+
mode: "update"
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function lockSessionEventWriteRows(db, input) {
|
|
912
|
+
const controlLock = input.controlLock;
|
|
913
|
+
const control = controlLock === "share" || controlLock === "update" ? await lockWorkspaceInferenceControl(db, input.workspaceId, controlLock) : null;
|
|
914
|
+
let workspace = null;
|
|
915
|
+
if ((input.workspaceLock ?? "key_share") === "key_share") {
|
|
916
|
+
const [lockedWorkspace] = await db.select().from(workspaces).where(eq3(workspaces.id, input.workspaceId)).for("key share").limit(1);
|
|
917
|
+
workspace = lockedWorkspace ?? null;
|
|
918
|
+
}
|
|
919
|
+
const sessionIds = [...new Set(input.sessionIds ?? [])].sort();
|
|
920
|
+
const sessionLock = input.sessionLock ?? "no_key_update";
|
|
921
|
+
let sessions2 = sessionIds.length > 0 ? await db.select().from(sessions).where(
|
|
922
|
+
and3(
|
|
923
|
+
eq3(sessions.workspaceId, input.workspaceId),
|
|
924
|
+
inArray2(sessions.id, sessionIds)
|
|
925
|
+
)
|
|
926
|
+
).orderBy(sessions.id).for(sessionLock === "key_share" ? "key share" : "no key update") : [];
|
|
927
|
+
const cursors = sessions2.length > 0 ? await db.select().from(sessionEventCursors).where(
|
|
928
|
+
and3(
|
|
929
|
+
eq3(sessionEventCursors.workspaceId, input.workspaceId),
|
|
930
|
+
inArray2(
|
|
931
|
+
sessionEventCursors.sessionId,
|
|
932
|
+
sessions2.map((session) => session.id)
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
).orderBy(sessionEventCursors.sessionId).for("update") : [];
|
|
936
|
+
if (cursors.length !== sessions2.length) {
|
|
937
|
+
throw new SessionControlInvariantError(
|
|
938
|
+
`Session event cursor lock set was incomplete for workspace ${input.workspaceId}`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (sessionLock === "key_share" && sessions2.length > 0) {
|
|
942
|
+
sessions2 = await db.select().from(sessions).where(
|
|
943
|
+
and3(
|
|
944
|
+
eq3(sessions.workspaceId, input.workspaceId),
|
|
945
|
+
inArray2(sessions.id, sessionIds)
|
|
946
|
+
)
|
|
947
|
+
).orderBy(sessions.id).for("key share");
|
|
948
|
+
}
|
|
949
|
+
const canonicalSessions = sessions2.map((session, index) => {
|
|
950
|
+
const cursor = cursors[index];
|
|
951
|
+
if (cursor.sessionId !== session.id || cursor.accountId !== session.accountId || cursor.workspaceId !== session.workspaceId) {
|
|
952
|
+
throw new SessionControlInvariantError(
|
|
953
|
+
`Session event cursor identity failed for session ${session.id}`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (cursor.lastSequence < session.lastSequence) {
|
|
957
|
+
throw new SessionControlInvariantError(
|
|
958
|
+
`Session event cursor is behind session projection for session ${session.id}`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
return { ...session, lastSequence: cursor.lastSequence };
|
|
962
|
+
});
|
|
963
|
+
const turnIds = [...new Set(input.turnIds ?? [])].sort();
|
|
964
|
+
const turns = turnIds.length > 0 ? await db.select().from(sessionTurns).where(
|
|
965
|
+
and3(
|
|
966
|
+
eq3(sessionTurns.workspaceId, input.workspaceId),
|
|
967
|
+
inArray2(sessionTurns.id, turnIds)
|
|
968
|
+
)
|
|
969
|
+
).orderBy(sessionTurns.id).for("update") : [];
|
|
970
|
+
const attemptIds = [...new Set(input.attemptIds ?? [])].sort();
|
|
971
|
+
const attempts = attemptIds.length > 0 ? await db.select().from(sessionTurnAttempts).where(
|
|
972
|
+
and3(
|
|
973
|
+
eq3(sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
974
|
+
inArray2(sessionTurnAttempts.id, attemptIds)
|
|
975
|
+
)
|
|
976
|
+
).orderBy(sessionTurnAttempts.id).for("update") : [];
|
|
977
|
+
return { control, workspace, sessions: canonicalSessions, cursors, turns, attempts };
|
|
978
|
+
}
|
|
979
|
+
async function registerSessionTurnAttemptClaim(db, input) {
|
|
980
|
+
const authoritySnapshot = assertSessionAuthoritySnapshot({
|
|
981
|
+
attemptId: input.id,
|
|
982
|
+
authorityEpoch: input.authorityEpoch,
|
|
983
|
+
authorityVisibility: input.authorityVisibility,
|
|
984
|
+
authorityOwnerOrganizationMembershipId: input.authorityOwnerOrganizationMembershipId
|
|
985
|
+
});
|
|
986
|
+
const [inserted] = await db.insert(sessionTurnAttempts).values({
|
|
987
|
+
...input,
|
|
988
|
+
...authoritySnapshot,
|
|
989
|
+
state: "claimed"
|
|
990
|
+
}).onConflictDoNothing({ target: sessionTurnAttempts.id }).returning();
|
|
991
|
+
if (inserted) return inserted;
|
|
992
|
+
const [existing] = await db.select().from(sessionTurnAttempts).where(
|
|
993
|
+
and3(
|
|
994
|
+
eq3(sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
995
|
+
eq3(sessionTurnAttempts.id, input.id)
|
|
996
|
+
)
|
|
997
|
+
).for("update").limit(1);
|
|
998
|
+
const existingAuthoritySnapshot = existing ? assertSessionAuthoritySnapshot({
|
|
999
|
+
attemptId: existing.id,
|
|
1000
|
+
authorityEpoch: existing.authorityEpoch,
|
|
1001
|
+
authorityVisibility: existing.authorityVisibility,
|
|
1002
|
+
authorityOwnerOrganizationMembershipId: existing.authorityOwnerOrganizationMembershipId
|
|
1003
|
+
}) : null;
|
|
1004
|
+
if (!existing || existing.accountId !== input.accountId || existing.sessionId !== input.sessionId || existing.turnId !== input.turnId || existing.executionGeneration !== input.executionGeneration || !existingAuthoritySnapshot || !sessionAuthoritySnapshotsEqual(existingAuthoritySnapshot, authoritySnapshot) || existing.temporalWorkflowId !== input.temporalWorkflowId || existing.temporalWorkflowRunId !== input.temporalWorkflowRunId || existing.temporalActivityId !== input.temporalActivityId || existing.personalResourceProtocolVersion !== input.personalResourceProtocolVersion || JSON.stringify(existing.connectorActionPolicies) !== JSON.stringify(input.connectorActionPolicies) || existing.state === "closed") {
|
|
1005
|
+
throw new SessionControlInvariantError(
|
|
1006
|
+
`Attempt ${input.id} conflicts with a different or closed ownership chain`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return existing;
|
|
1010
|
+
}
|
|
1011
|
+
function assertSessionAuthoritySnapshot(input) {
|
|
1012
|
+
const valid = Number.isSafeInteger(input.authorityEpoch) && input.authorityEpoch > 0 && (input.authorityVisibility === "user_private" || input.authorityVisibility === "workspace_shared") && (input.authorityVisibility === "user_private" && typeof input.authorityOwnerOrganizationMembershipId === "string" || input.authorityVisibility === "workspace_shared" && (input.authorityOwnerOrganizationMembershipId === null || typeof input.authorityOwnerOrganizationMembershipId === "string"));
|
|
1013
|
+
if (!valid) {
|
|
1014
|
+
throw new SessionControlInvariantError(
|
|
1015
|
+
`Attempt ${input.attemptId ?? "unknown"} has an invalid session authority snapshot`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
authorityEpoch: input.authorityEpoch,
|
|
1020
|
+
authorityVisibility: input.authorityVisibility,
|
|
1021
|
+
authorityOwnerOrganizationMembershipId: input.authorityOwnerOrganizationMembershipId
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
function sessionAuthoritySnapshotMatchesSession(snapshot, session) {
|
|
1025
|
+
return snapshot.authorityEpoch === session.authorityEpoch && snapshot.authorityVisibility === session.visibility && snapshot.authorityOwnerOrganizationMembershipId === (session.ownerOrganizationMembershipId ?? null);
|
|
1026
|
+
}
|
|
1027
|
+
function sessionAuthoritySnapshotsEqual(left, right) {
|
|
1028
|
+
return left.authorityEpoch === right.authorityEpoch && left.authorityVisibility === right.authorityVisibility && left.authorityOwnerOrganizationMembershipId === right.authorityOwnerOrganizationMembershipId;
|
|
1029
|
+
}
|
|
1030
|
+
async function closeSessionTurnAttemptInTransaction(db, input) {
|
|
1031
|
+
const [attempt] = await db.select().from(sessionTurnAttempts).where(
|
|
1032
|
+
and3(
|
|
1033
|
+
eq3(sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
1034
|
+
eq3(sessionTurnAttempts.id, input.id)
|
|
1035
|
+
)
|
|
1036
|
+
).for("update").limit(1);
|
|
1037
|
+
if (!attempt || attempt.accountId !== input.accountId || attempt.sessionId !== input.sessionId || attempt.turnId !== input.turnId || attempt.executionGeneration !== input.executionGeneration) {
|
|
1038
|
+
throw new SessionControlInvariantError(
|
|
1039
|
+
`Attempt ${input.id} does not own the expected session turn generation`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (attempt.state === "closed") {
|
|
1043
|
+
if (attempt.outcome !== input.outcome) {
|
|
1044
|
+
throw new SessionControlInvariantError(
|
|
1045
|
+
`Attempt ${input.id} is already closed as ${attempt.outcome ?? "unknown"}`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
return { action: "already_closed" };
|
|
1049
|
+
}
|
|
1050
|
+
if (attempt.state !== "claimed" && attempt.state !== "running") {
|
|
1051
|
+
throw new SessionControlInvariantError(
|
|
1052
|
+
`Attempt ${input.id} has invalid live state ${attempt.state}`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const now = input.closedAt ?? /* @__PURE__ */ new Date();
|
|
1056
|
+
const [closed] = await db.update(sessionTurnAttempts).set({
|
|
1057
|
+
state: "closed",
|
|
1058
|
+
outcome: input.outcome,
|
|
1059
|
+
workerId: null,
|
|
1060
|
+
leaseId: null,
|
|
1061
|
+
leaseExpiresAt: null,
|
|
1062
|
+
closedAt: now,
|
|
1063
|
+
updatedAt: now
|
|
1064
|
+
}).where(
|
|
1065
|
+
and3(
|
|
1066
|
+
eq3(sessionTurnAttempts.workspaceId, input.workspaceId),
|
|
1067
|
+
eq3(sessionTurnAttempts.id, input.id),
|
|
1068
|
+
sql3`${sessionTurnAttempts.state} in ('claimed', 'running')`
|
|
1069
|
+
)
|
|
1070
|
+
).returning({ id: sessionTurnAttempts.id });
|
|
1071
|
+
if (!closed) {
|
|
1072
|
+
throw new SessionControlInvariantError(`Attempt ${input.id} changed while locked`);
|
|
1073
|
+
}
|
|
1074
|
+
return { action: "closed" };
|
|
1075
|
+
}
|
|
1076
|
+
function targetValues(sessionIds) {
|
|
1077
|
+
if (sessionIds.length === 0) {
|
|
1078
|
+
throw new SessionControlInvariantError("At least one target session is required");
|
|
1079
|
+
}
|
|
1080
|
+
return sql3.join(
|
|
1081
|
+
sessionIds.map((id) => sql3`(${id}::uuid)`),
|
|
1082
|
+
sql3`, `
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
var TARGET_PATH_PROJECTION_LIMIT = 128;
|
|
1086
|
+
async function loadTargetAncestryRows(db, workspaceId, sessionIds) {
|
|
1087
|
+
return await db.execute(sql3`
|
|
1088
|
+
with recursive targets(id) as (values ${targetValues(sessionIds)}),
|
|
1089
|
+
ancestry as (
|
|
1090
|
+
select
|
|
1091
|
+
target.id as target_id,
|
|
1092
|
+
session.id as session_id,
|
|
1093
|
+
session.parent_session_id,
|
|
1094
|
+
session.title,
|
|
1095
|
+
session.direct_control_state,
|
|
1096
|
+
session.direct_pause_revision,
|
|
1097
|
+
session.subtree_run_override_revision,
|
|
1098
|
+
session.control_version,
|
|
1099
|
+
session.direct_control_changed_by,
|
|
1100
|
+
session.direct_control_reason,
|
|
1101
|
+
session.direct_control_changed_at,
|
|
1102
|
+
0::integer as depth,
|
|
1103
|
+
array[session.id]::uuid[] as path,
|
|
1104
|
+
false as cycle
|
|
1105
|
+
from targets target
|
|
1106
|
+
join ${sessions} session
|
|
1107
|
+
on session.workspace_id = ${workspaceId} and session.id = target.id
|
|
1108
|
+
union all
|
|
1109
|
+
select
|
|
1110
|
+
child.target_id,
|
|
1111
|
+
parent.id,
|
|
1112
|
+
parent.parent_session_id,
|
|
1113
|
+
parent.title,
|
|
1114
|
+
parent.direct_control_state,
|
|
1115
|
+
parent.direct_pause_revision,
|
|
1116
|
+
parent.subtree_run_override_revision,
|
|
1117
|
+
parent.control_version,
|
|
1118
|
+
parent.direct_control_changed_by,
|
|
1119
|
+
parent.direct_control_reason,
|
|
1120
|
+
parent.direct_control_changed_at,
|
|
1121
|
+
child.depth + 1,
|
|
1122
|
+
child.path || parent.id,
|
|
1123
|
+
parent.id = any(child.path)
|
|
1124
|
+
from ancestry child
|
|
1125
|
+
join ${sessions} parent
|
|
1126
|
+
on parent.workspace_id = ${workspaceId} and parent.id = child.parent_session_id
|
|
1127
|
+
where child.parent_session_id is not null
|
|
1128
|
+
and not child.cycle
|
|
1129
|
+
and child.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
1130
|
+
)
|
|
1131
|
+
select
|
|
1132
|
+
target_id as "targetId",
|
|
1133
|
+
session_id as "sessionId",
|
|
1134
|
+
parent_session_id as "parentSessionId",
|
|
1135
|
+
title,
|
|
1136
|
+
direct_control_state as "directState",
|
|
1137
|
+
direct_pause_revision as "directPauseRevision",
|
|
1138
|
+
subtree_run_override_revision as "subtreeRunOverrideRevision",
|
|
1139
|
+
control_version as "controlVersion",
|
|
1140
|
+
direct_control_changed_by as "directControlChangedBy",
|
|
1141
|
+
direct_control_reason as "directControlReason",
|
|
1142
|
+
direct_control_changed_at as "directControlChangedAt",
|
|
1143
|
+
depth,
|
|
1144
|
+
cycle
|
|
1145
|
+
from ancestry
|
|
1146
|
+
order by target_id, depth
|
|
1147
|
+
`);
|
|
1148
|
+
}
|
|
1149
|
+
async function loadAncestryNodes(db, workspaceId, sessionIds) {
|
|
1150
|
+
const rows = await db.execute(sql3`
|
|
1151
|
+
with recursive targets(id) as (values ${targetValues(sessionIds)}),
|
|
1152
|
+
ancestry_ids(id) as (
|
|
1153
|
+
select session.id
|
|
1154
|
+
from targets target
|
|
1155
|
+
join ${sessions} session
|
|
1156
|
+
on session.workspace_id = ${workspaceId} and session.id = target.id
|
|
1157
|
+
union
|
|
1158
|
+
select parent.id
|
|
1159
|
+
from ancestry_ids child
|
|
1160
|
+
join ${sessions} current
|
|
1161
|
+
on current.workspace_id = ${workspaceId} and current.id = child.id
|
|
1162
|
+
join ${sessions} parent
|
|
1163
|
+
on parent.workspace_id = current.workspace_id
|
|
1164
|
+
and parent.id = current.parent_session_id
|
|
1165
|
+
)
|
|
1166
|
+
select
|
|
1167
|
+
session.id as "sessionId",
|
|
1168
|
+
session.parent_session_id as "parentSessionId",
|
|
1169
|
+
session.title,
|
|
1170
|
+
session.direct_control_state as "directState",
|
|
1171
|
+
session.direct_pause_revision as "directPauseRevision",
|
|
1172
|
+
session.subtree_run_override_revision as "subtreeRunOverrideRevision",
|
|
1173
|
+
session.control_version as "controlVersion",
|
|
1174
|
+
session.direct_control_changed_by as "directControlChangedBy",
|
|
1175
|
+
session.direct_control_reason as "directControlReason",
|
|
1176
|
+
session.direct_control_changed_at as "directControlChangedAt"
|
|
1177
|
+
from ancestry_ids ancestry
|
|
1178
|
+
join ${sessions} session
|
|
1179
|
+
on session.workspace_id = ${workspaceId} and session.id = ancestry.id
|
|
1180
|
+
`);
|
|
1181
|
+
return new Map(rows.map((row) => [row.sessionId, row]));
|
|
1182
|
+
}
|
|
1183
|
+
function ancestryRowsForTarget(targetId, nodes) {
|
|
1184
|
+
const rows = [];
|
|
1185
|
+
const path = /* @__PURE__ */ new Set();
|
|
1186
|
+
let sessionId = targetId;
|
|
1187
|
+
for (let depth = 0; sessionId !== null; depth += 1) {
|
|
1188
|
+
const node = nodes.get(sessionId);
|
|
1189
|
+
if (!node) break;
|
|
1190
|
+
const cycle = path.has(sessionId);
|
|
1191
|
+
rows.push({ ...node, targetId, depth, cycle });
|
|
1192
|
+
if (cycle || depth >= SESSION_ANCESTRY_LIMIT) break;
|
|
1193
|
+
path.add(sessionId);
|
|
1194
|
+
sessionId = node.parentSessionId;
|
|
1195
|
+
}
|
|
1196
|
+
return rows;
|
|
1197
|
+
}
|
|
1198
|
+
function assertCompleteAncestry(sessionId, rows) {
|
|
1199
|
+
if (rows.length === 0) {
|
|
1200
|
+
throw new SessionControlInvariantError(`Session ${sessionId} does not exist in its workspace`);
|
|
1201
|
+
}
|
|
1202
|
+
if (rows.some((row) => row.cycle)) {
|
|
1203
|
+
throw new SessionControlInvariantError(`Session ${sessionId} has cyclic ancestry`);
|
|
1204
|
+
}
|
|
1205
|
+
const last = rows.at(-1);
|
|
1206
|
+
const depth = Number(last.depth);
|
|
1207
|
+
if (last.parentSessionId !== null) {
|
|
1208
|
+
throw new SessionControlInvariantError(
|
|
1209
|
+
depth >= SESSION_ANCESTRY_LIMIT ? `Session ${sessionId} ancestry exceeds ${SESSION_ANCESTRY_LIMIT}` : `Session ${sessionId} has a missing ancestor ${last.parentSessionId}`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function blockerForSession(row, revision) {
|
|
1214
|
+
return {
|
|
1215
|
+
kind: "session",
|
|
1216
|
+
sessionId: row.sessionId,
|
|
1217
|
+
displayName: row.title?.trim() || "Untitled session",
|
|
1218
|
+
actor: row.directControlChangedBy,
|
|
1219
|
+
reason: row.directControlReason,
|
|
1220
|
+
changedAt: asDate(row.directControlChangedAt),
|
|
1221
|
+
revision
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function projectEffectiveControl(workspace, targetId, rows, settlementAttempts, stoppingBackgroundCommandCount) {
|
|
1225
|
+
assertCompleteAncestry(targetId, rows);
|
|
1226
|
+
const workspaceRevision = asSafeRevision(workspace.revision, "workspace control revision");
|
|
1227
|
+
const path = rows.map((row) => ({
|
|
1228
|
+
row,
|
|
1229
|
+
depth: Number(row.depth),
|
|
1230
|
+
pauseRevision: asSafeRevision(row.directPauseRevision, "direct pause revision"),
|
|
1231
|
+
overrideRevision: asSafeRevision(
|
|
1232
|
+
row.subtreeRunOverrideRevision,
|
|
1233
|
+
"subtree run override revision"
|
|
1234
|
+
),
|
|
1235
|
+
controlVersion: asSafeRevision(row.controlVersion, "session control version")
|
|
1236
|
+
}));
|
|
1237
|
+
const undefeated = [];
|
|
1238
|
+
for (const candidate of path) {
|
|
1239
|
+
if (candidate.row.directState !== "paused" || candidate.pauseRevision === null) continue;
|
|
1240
|
+
const defeated = path.some(
|
|
1241
|
+
(possibleOverride) => possibleOverride.depth < candidate.depth && possibleOverride.overrideRevision !== null && possibleOverride.overrideRevision > candidate.pauseRevision
|
|
1242
|
+
);
|
|
1243
|
+
if (!defeated) {
|
|
1244
|
+
undefeated.push({
|
|
1245
|
+
blocker: blockerForSession(candidate.row, candidate.pauseRevision),
|
|
1246
|
+
depth: candidate.depth
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const workspacePauseRevision = asSafeRevision(
|
|
1251
|
+
workspace.workspacePauseRevision,
|
|
1252
|
+
"workspace pause revision"
|
|
1253
|
+
);
|
|
1254
|
+
if (workspace.workspaceState === "paused") {
|
|
1255
|
+
if (workspacePauseRevision === null) {
|
|
1256
|
+
throw new SessionControlInvariantError("Paused workspace is missing its pause revision");
|
|
1257
|
+
}
|
|
1258
|
+
const defeated = path.some(
|
|
1259
|
+
(candidate) => candidate.overrideRevision !== null && candidate.overrideRevision > workspacePauseRevision
|
|
1260
|
+
);
|
|
1261
|
+
if (!defeated) {
|
|
1262
|
+
undefeated.push({
|
|
1263
|
+
blocker: {
|
|
1264
|
+
kind: "workspace",
|
|
1265
|
+
displayName: "Workspace",
|
|
1266
|
+
actor: workspace.changedBy,
|
|
1267
|
+
reason: workspace.reason,
|
|
1268
|
+
changedAt: asDate(workspace.changedAt),
|
|
1269
|
+
revision: workspacePauseRevision
|
|
1270
|
+
},
|
|
1271
|
+
depth: Number.POSITIVE_INFINITY
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
undefeated.sort((left, right) => left.depth - right.depth);
|
|
1276
|
+
const blockers = undefeated.map(({ blocker }) => blocker);
|
|
1277
|
+
const primaryBlocker = blockers[0] ?? null;
|
|
1278
|
+
const target = path[0];
|
|
1279
|
+
const override = path.filter((candidate) => candidate.overrideRevision !== null).sort((left, right) => right.overrideRevision - left.overrideRevision)[0];
|
|
1280
|
+
const options = [];
|
|
1281
|
+
if (blockers.length > 0) {
|
|
1282
|
+
options.push({
|
|
1283
|
+
scope: "selected",
|
|
1284
|
+
targetId,
|
|
1285
|
+
selectedStateAfter: "active",
|
|
1286
|
+
impactCopy: "Resume this session and its descendants without changing sibling workstreams."
|
|
1287
|
+
});
|
|
1288
|
+
for (const entry of undefeated) {
|
|
1289
|
+
if (entry.blocker.kind !== "session" || entry.blocker.sessionId === targetId) continue;
|
|
1290
|
+
const remaining = undefeated.find(
|
|
1291
|
+
(candidate) => candidate.depth < entry.depth && candidate.blocker.sessionId !== entry.blocker.sessionId
|
|
1292
|
+
)?.blocker;
|
|
1293
|
+
options.push({
|
|
1294
|
+
scope: "session",
|
|
1295
|
+
targetId: entry.blocker.sessionId,
|
|
1296
|
+
selectedStateAfter: remaining ? "paused" : "active",
|
|
1297
|
+
...remaining ? { remainingPrimaryBlocker: remaining } : {},
|
|
1298
|
+
impactCopy: `Resume the workstream rooted at ${entry.blocker.displayName}.`
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (workspace.workspaceState === "paused") {
|
|
1302
|
+
const remaining = undefeated.find((entry) => entry.blocker.kind === "session")?.blocker;
|
|
1303
|
+
options.push({
|
|
1304
|
+
scope: "workspace",
|
|
1305
|
+
selectedStateAfter: remaining ? "paused" : "active",
|
|
1306
|
+
...remaining ? { remainingPrimaryBlocker: remaining } : {},
|
|
1307
|
+
impactCopy: "Resume the entire workspace; narrower session pauses remain in force."
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const etagFacts = {
|
|
1312
|
+
workspace: {
|
|
1313
|
+
state: workspace.workspaceState,
|
|
1314
|
+
pauseRevision: workspacePauseRevision
|
|
1315
|
+
},
|
|
1316
|
+
path: path.map((candidate) => ({
|
|
1317
|
+
sessionId: candidate.row.sessionId,
|
|
1318
|
+
directState: candidate.row.directState,
|
|
1319
|
+
pauseRevision: candidate.pauseRevision,
|
|
1320
|
+
overrideRevision: candidate.overrideRevision
|
|
1321
|
+
}))
|
|
1322
|
+
};
|
|
1323
|
+
return {
|
|
1324
|
+
state: blockers.length > 0 ? "paused" : "active",
|
|
1325
|
+
controlVersion: Math.max(
|
|
1326
|
+
workspaceRevision,
|
|
1327
|
+
...path.map((candidate) => candidate.controlVersion)
|
|
1328
|
+
),
|
|
1329
|
+
controlEtag: controlEtag(etagFacts),
|
|
1330
|
+
directState: target.row.directState === "paused" ? "paused" : "active",
|
|
1331
|
+
primaryBlocker,
|
|
1332
|
+
additionalBlockerCount: Math.max(0, blockers.length - 1),
|
|
1333
|
+
blockers,
|
|
1334
|
+
resumeOptions: options,
|
|
1335
|
+
override: override?.overrideRevision === null || !override ? null : {
|
|
1336
|
+
rootSessionId: override.row.sessionId,
|
|
1337
|
+
revision: override.overrideRevision
|
|
1338
|
+
},
|
|
1339
|
+
settlement: settlementAttempts.attemptCount > 0 ? { state: "stopping", ...settlementAttempts } : null,
|
|
1340
|
+
backgroundCommandSettlement: stoppingBackgroundCommandCount > 0 ? { state: "stopping", commandCount: stoppingBackgroundCommandCount } : null
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async function stoppingBackgroundCommandCounts(db, workspaceId, sessionIds) {
|
|
1344
|
+
const rows = await db.execute(sql3`
|
|
1345
|
+
with recursive targets(id) as (values ${targetValues(sessionIds)}),
|
|
1346
|
+
descendants(target_id, session_id, depth, path) as (
|
|
1347
|
+
select target.id, target.id, 0::integer, array[target.id]::uuid[] from targets target
|
|
1348
|
+
union all
|
|
1349
|
+
select parent.target_id, child.id, parent.depth + 1, parent.path || child.id
|
|
1350
|
+
from descendants parent
|
|
1351
|
+
join ${sessions} child
|
|
1352
|
+
on child.workspace_id = ${workspaceId} and child.parent_session_id = parent.session_id
|
|
1353
|
+
where not child.id = any(parent.path) and parent.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
1354
|
+
)
|
|
1355
|
+
select descendant.target_id as "sessionId", count(distinct command.id)::integer as "commandCount"
|
|
1356
|
+
from descendants descendant
|
|
1357
|
+
join ${sessionBackgroundCommands} command
|
|
1358
|
+
on command.workspace_id = ${workspaceId} and command.session_id = descendant.session_id
|
|
1359
|
+
and command.state = 'stopping'
|
|
1360
|
+
group by descendant.target_id
|
|
1361
|
+
`);
|
|
1362
|
+
return new Map(
|
|
1363
|
+
rows.map((row) => [
|
|
1364
|
+
row.sessionId,
|
|
1365
|
+
Number(row.commandCount)
|
|
1366
|
+
])
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
async function settlementAttemptCounts(db, workspaceId, sessionIds) {
|
|
1370
|
+
const rows = await db.execute(sql3`
|
|
1371
|
+
with recursive targets(id) as (values ${targetValues(sessionIds)}),
|
|
1372
|
+
descendant_sessions(target_id, session_id, depth, path) as (
|
|
1373
|
+
select target.id, target.id, 0::integer, array[target.id]::uuid[]
|
|
1374
|
+
from targets target
|
|
1375
|
+
union all
|
|
1376
|
+
select
|
|
1377
|
+
descendant.target_id,
|
|
1378
|
+
child.id,
|
|
1379
|
+
descendant.depth + 1,
|
|
1380
|
+
descendant.path || child.id
|
|
1381
|
+
from descendant_sessions descendant
|
|
1382
|
+
join ${sessions} child
|
|
1383
|
+
on child.workspace_id = ${workspaceId}
|
|
1384
|
+
and child.parent_session_id = descendant.session_id
|
|
1385
|
+
where not child.id = any(descendant.path)
|
|
1386
|
+
and descendant.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
1387
|
+
), interruptions as (
|
|
1388
|
+
select
|
|
1389
|
+
descendant.target_id,
|
|
1390
|
+
interruption.attempt_id,
|
|
1391
|
+
bool_or(interruption.state in ('pending', 'delivered', 'acknowledged'))
|
|
1392
|
+
as interruption_pending,
|
|
1393
|
+
bool_or(
|
|
1394
|
+
interruption.state in ('settled', 'rejected_stale')
|
|
1395
|
+
and attempt.quiesced_at is null
|
|
1396
|
+
) as quiescence_pending
|
|
1397
|
+
from descendant_sessions descendant
|
|
1398
|
+
join ${sessionAttemptInterruptions} interruption
|
|
1399
|
+
on interruption.workspace_id = ${workspaceId}
|
|
1400
|
+
and interruption.session_id = descendant.session_id
|
|
1401
|
+
join ${sessionTurnAttempts} attempt
|
|
1402
|
+
on attempt.workspace_id = interruption.workspace_id
|
|
1403
|
+
and attempt.id = interruption.attempt_id
|
|
1404
|
+
where (
|
|
1405
|
+
interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
1406
|
+
or (
|
|
1407
|
+
interruption.state in ('settled', 'rejected_stale')
|
|
1408
|
+
and attempt.quiesced_at is null
|
|
1409
|
+
)
|
|
1410
|
+
)
|
|
1411
|
+
group by descendant.target_id, interruption.attempt_id
|
|
1412
|
+
)
|
|
1413
|
+
select
|
|
1414
|
+
target.id as "sessionId",
|
|
1415
|
+
count(interruption.attempt_id)::integer as "attemptCount",
|
|
1416
|
+
count(interruption.attempt_id)
|
|
1417
|
+
filter (where interruption.interruption_pending)::integer as "interruptionPendingCount",
|
|
1418
|
+
count(interruption.attempt_id)
|
|
1419
|
+
filter (where interruption.quiescence_pending)::integer as "quiescencePendingCount"
|
|
1420
|
+
from targets target
|
|
1421
|
+
join interruptions interruption on interruption.target_id = target.id
|
|
1422
|
+
group by target.id
|
|
1423
|
+
`);
|
|
1424
|
+
return new Map(
|
|
1425
|
+
rows.map(
|
|
1426
|
+
(row) => [
|
|
1427
|
+
row.sessionId,
|
|
1428
|
+
{
|
|
1429
|
+
attemptCount: Number(row.attemptCount),
|
|
1430
|
+
interruptionPendingCount: Number(row.interruptionPendingCount),
|
|
1431
|
+
quiescencePendingCount: Number(row.quiescencePendingCount)
|
|
1432
|
+
}
|
|
1433
|
+
]
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
async function evaluateSessionControls(db, workspaceId, sessionIds, options = {}) {
|
|
1438
|
+
const uniqueIds = [...new Set(sessionIds)];
|
|
1439
|
+
if (uniqueIds.length === 0) {
|
|
1440
|
+
return /* @__PURE__ */ new Map();
|
|
1441
|
+
}
|
|
1442
|
+
if (options.workspaceControl && options.workspaceControl.workspaceId !== workspaceId) {
|
|
1443
|
+
throw new SessionControlInvariantError(
|
|
1444
|
+
`Locked workspace control ${options.workspaceControl.workspaceId} does not match ${workspaceId}`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const workspace = options.workspaceControl ?? await lockWorkspaceInferenceControl(db, workspaceId, options.lock ?? "share");
|
|
1448
|
+
const stopping = await settlementAttemptCounts(db, workspaceId, uniqueIds);
|
|
1449
|
+
const stoppingCommands = await stoppingBackgroundCommandCounts(db, workspaceId, uniqueIds);
|
|
1450
|
+
const result = /* @__PURE__ */ new Map();
|
|
1451
|
+
if (uniqueIds.length <= TARGET_PATH_PROJECTION_LIMIT) {
|
|
1452
|
+
const ancestry2 = await loadTargetAncestryRows(db, workspaceId, uniqueIds);
|
|
1453
|
+
const ancestryByTarget = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const row of ancestry2) {
|
|
1455
|
+
const rows = ancestryByTarget.get(row.targetId);
|
|
1456
|
+
if (rows) rows.push(row);
|
|
1457
|
+
else ancestryByTarget.set(row.targetId, [row]);
|
|
1458
|
+
}
|
|
1459
|
+
for (const sessionId of uniqueIds) {
|
|
1460
|
+
result.set(
|
|
1461
|
+
sessionId,
|
|
1462
|
+
projectEffectiveControl(
|
|
1463
|
+
workspace,
|
|
1464
|
+
sessionId,
|
|
1465
|
+
ancestryByTarget.get(sessionId) ?? [],
|
|
1466
|
+
stopping.get(sessionId) ?? NO_SETTLEMENT_ATTEMPTS,
|
|
1467
|
+
stoppingCommands.get(sessionId) ?? 0
|
|
1468
|
+
)
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
const ancestry = await loadAncestryNodes(db, workspaceId, uniqueIds);
|
|
1474
|
+
for (const sessionId of uniqueIds) {
|
|
1475
|
+
result.set(
|
|
1476
|
+
sessionId,
|
|
1477
|
+
projectEffectiveControl(
|
|
1478
|
+
workspace,
|
|
1479
|
+
sessionId,
|
|
1480
|
+
ancestryRowsForTarget(sessionId, ancestry),
|
|
1481
|
+
stopping.get(sessionId) ?? NO_SETTLEMENT_ATTEMPTS,
|
|
1482
|
+
stoppingCommands.get(sessionId) ?? 0
|
|
1483
|
+
)
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
return result;
|
|
1487
|
+
}
|
|
1488
|
+
async function evaluateSessionWriteAdmissionControl(db, workspaceId, sessionId, options = {}) {
|
|
1489
|
+
if (options.workspaceControl && options.workspaceControl.workspaceId !== workspaceId) {
|
|
1490
|
+
throw new SessionControlInvariantError(
|
|
1491
|
+
`Locked workspace control ${options.workspaceControl.workspaceId} does not match ${workspaceId}`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const workspace = options.workspaceControl ?? await lockWorkspaceInferenceControl(db, workspaceId, options.lock ?? "share");
|
|
1495
|
+
const projected = projectEffectiveControl(
|
|
1496
|
+
workspace,
|
|
1497
|
+
sessionId,
|
|
1498
|
+
await loadTargetAncestryRows(db, workspaceId, [sessionId]),
|
|
1499
|
+
NO_SETTLEMENT_ATTEMPTS,
|
|
1500
|
+
0
|
|
1501
|
+
);
|
|
1502
|
+
return {
|
|
1503
|
+
state: projected.state,
|
|
1504
|
+
controlVersion: projected.controlVersion,
|
|
1505
|
+
primaryBlockerKind: projected.primaryBlocker?.kind ?? null
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
async function evaluateSessionDiscoveryControls(db, workspaceId, sessionIds) {
|
|
1509
|
+
const uniqueIds = [...new Set(sessionIds)];
|
|
1510
|
+
if (uniqueIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1511
|
+
if (uniqueIds.length > SESSION_DISCOVERY_CONTROL_TARGET_LIMIT) {
|
|
1512
|
+
throw new SessionControlInvariantError(
|
|
1513
|
+
`Session discovery control projection exceeds ${SESSION_DISCOVERY_CONTROL_TARGET_LIMIT} targets`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const workspace = await lockWorkspaceInferenceControl(db, workspaceId, "none");
|
|
1517
|
+
const workspacePauseRevision = asSafeRevision(
|
|
1518
|
+
workspace.workspacePauseRevision,
|
|
1519
|
+
"workspace pause revision"
|
|
1520
|
+
);
|
|
1521
|
+
if (workspace.workspaceState === "paused" && workspacePauseRevision === null) {
|
|
1522
|
+
throw new SessionControlInvariantError("Paused workspace is missing its pause revision");
|
|
1523
|
+
}
|
|
1524
|
+
const rows = await db.execute(sql3`
|
|
1525
|
+
with recursive targets(id) as (values ${targetValues(uniqueIds)}),
|
|
1526
|
+
ancestry as (
|
|
1527
|
+
select
|
|
1528
|
+
target.id as target_id,
|
|
1529
|
+
session.id as session_id,
|
|
1530
|
+
session.parent_session_id,
|
|
1531
|
+
left(coalesce(nullif(btrim(session.title), ''), 'Untitled session'), ${SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS}) as display_name,
|
|
1532
|
+
char_length(coalesce(nullif(btrim(session.title), ''), 'Untitled session'))::integer as display_name_original_chars,
|
|
1533
|
+
session.direct_control_state,
|
|
1534
|
+
session.direct_pause_revision,
|
|
1535
|
+
session.subtree_run_override_revision,
|
|
1536
|
+
0::integer as depth
|
|
1537
|
+
from targets target
|
|
1538
|
+
join ${sessions} session
|
|
1539
|
+
on session.workspace_id = ${workspaceId} and session.id = target.id
|
|
1540
|
+
union all
|
|
1541
|
+
select
|
|
1542
|
+
child.target_id,
|
|
1543
|
+
parent.id,
|
|
1544
|
+
parent.parent_session_id,
|
|
1545
|
+
left(coalesce(nullif(btrim(parent.title), ''), 'Untitled session'), ${SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS}),
|
|
1546
|
+
char_length(coalesce(nullif(btrim(parent.title), ''), 'Untitled session'))::integer,
|
|
1547
|
+
parent.direct_control_state,
|
|
1548
|
+
parent.direct_pause_revision,
|
|
1549
|
+
parent.subtree_run_override_revision,
|
|
1550
|
+
child.depth + 1
|
|
1551
|
+
from ancestry child
|
|
1552
|
+
join ${sessions} parent
|
|
1553
|
+
on parent.workspace_id = ${workspaceId} and parent.id = child.parent_session_id
|
|
1554
|
+
where child.parent_session_id is not null
|
|
1555
|
+
and child.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
1556
|
+
),
|
|
1557
|
+
path as (
|
|
1558
|
+
select
|
|
1559
|
+
ancestry.*,
|
|
1560
|
+
max(ancestry.subtree_run_override_revision) over (
|
|
1561
|
+
partition by ancestry.target_id
|
|
1562
|
+
order by ancestry.depth
|
|
1563
|
+
rows between unbounded preceding and 1 preceding
|
|
1564
|
+
) as descendant_override_revision
|
|
1565
|
+
from ancestry
|
|
1566
|
+
),
|
|
1567
|
+
session_blockers as (
|
|
1568
|
+
select
|
|
1569
|
+
path.target_id,
|
|
1570
|
+
'session'::text as kind,
|
|
1571
|
+
path.session_id,
|
|
1572
|
+
path.display_name,
|
|
1573
|
+
path.display_name_original_chars,
|
|
1574
|
+
path.depth
|
|
1575
|
+
from path
|
|
1576
|
+
where path.direct_control_state = 'paused'
|
|
1577
|
+
and path.direct_pause_revision is not null
|
|
1578
|
+
and (
|
|
1579
|
+
path.descendant_override_revision is null
|
|
1580
|
+
or path.descendant_override_revision <= path.direct_pause_revision
|
|
1581
|
+
)
|
|
1582
|
+
),
|
|
1583
|
+
workspace_blockers as (
|
|
1584
|
+
select
|
|
1585
|
+
target.id as target_id,
|
|
1586
|
+
'workspace'::text as kind,
|
|
1587
|
+
null::uuid as session_id,
|
|
1588
|
+
'Workspace'::text as display_name,
|
|
1589
|
+
9::integer as display_name_original_chars,
|
|
1590
|
+
${SESSION_ANCESTRY_LIMIT + 1}::integer as depth
|
|
1591
|
+
from targets target
|
|
1592
|
+
where ${workspace.workspaceState === "paused"}
|
|
1593
|
+
and not exists (
|
|
1594
|
+
select 1
|
|
1595
|
+
from path
|
|
1596
|
+
where path.target_id = target.id
|
|
1597
|
+
and path.subtree_run_override_revision is not null
|
|
1598
|
+
and path.subtree_run_override_revision > ${workspacePauseRevision}
|
|
1599
|
+
)
|
|
1600
|
+
),
|
|
1601
|
+
blockers as (
|
|
1602
|
+
select * from session_blockers
|
|
1603
|
+
union all
|
|
1604
|
+
select * from workspace_blockers
|
|
1605
|
+
),
|
|
1606
|
+
primary_blockers as (
|
|
1607
|
+
select distinct on (blockers.target_id)
|
|
1608
|
+
blockers.target_id,
|
|
1609
|
+
blockers.kind,
|
|
1610
|
+
blockers.session_id,
|
|
1611
|
+
blockers.display_name,
|
|
1612
|
+
blockers.display_name_original_chars
|
|
1613
|
+
from blockers
|
|
1614
|
+
order by blockers.target_id, blockers.depth
|
|
1615
|
+
),
|
|
1616
|
+
blocker_counts as (
|
|
1617
|
+
select blockers.target_id, count(*)::integer as blocker_count
|
|
1618
|
+
from blockers
|
|
1619
|
+
group by blockers.target_id
|
|
1620
|
+
),
|
|
1621
|
+
ancestry_metadata as (
|
|
1622
|
+
select
|
|
1623
|
+
ancestry.target_id,
|
|
1624
|
+
count(*)::integer as ancestry_count,
|
|
1625
|
+
bool_or(ancestry.parent_session_id is null) as reached_root,
|
|
1626
|
+
max(ancestry.depth)::integer as max_depth
|
|
1627
|
+
from ancestry
|
|
1628
|
+
group by ancestry.target_id
|
|
1629
|
+
)
|
|
1630
|
+
select
|
|
1631
|
+
target.id as "targetId",
|
|
1632
|
+
coalesce(metadata.ancestry_count, 0)::integer as "ancestryCount",
|
|
1633
|
+
coalesce(metadata.reached_root, false) as "reachedRoot",
|
|
1634
|
+
metadata.max_depth as "maxDepth",
|
|
1635
|
+
coalesce(counts.blocker_count, 0)::integer as "blockerCount",
|
|
1636
|
+
primary_blocker.kind as "primaryKind",
|
|
1637
|
+
primary_blocker.session_id as "primarySessionId",
|
|
1638
|
+
primary_blocker.display_name as "primaryDisplayName",
|
|
1639
|
+
primary_blocker.display_name_original_chars as "primaryDisplayNameOriginalChars"
|
|
1640
|
+
from targets target
|
|
1641
|
+
left join ancestry_metadata metadata on metadata.target_id = target.id
|
|
1642
|
+
left join blocker_counts counts on counts.target_id = target.id
|
|
1643
|
+
left join primary_blockers primary_blocker on primary_blocker.target_id = target.id
|
|
1644
|
+
order by target.id
|
|
1645
|
+
`);
|
|
1646
|
+
const result = /* @__PURE__ */ new Map();
|
|
1647
|
+
for (const row of rows) {
|
|
1648
|
+
const ancestryCount = Number(row.ancestryCount);
|
|
1649
|
+
const maxDepth = row.maxDepth === null ? null : Number(row.maxDepth);
|
|
1650
|
+
if (ancestryCount === 0) {
|
|
1651
|
+
throw new SessionControlInvariantError(
|
|
1652
|
+
`Session ${row.targetId} does not exist in its workspace`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
if (!row.reachedRoot) {
|
|
1656
|
+
throw new SessionControlInvariantError(
|
|
1657
|
+
maxDepth !== null && maxDepth >= SESSION_ANCESTRY_LIMIT ? `Session ${row.targetId} ancestry exceeds ${SESSION_ANCESTRY_LIMIT}` : `Session ${row.targetId} has a missing ancestor`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
const blockerCount = Number(row.blockerCount);
|
|
1661
|
+
const primaryBlocker = row.primaryKind ? {
|
|
1662
|
+
kind: row.primaryKind,
|
|
1663
|
+
...row.primarySessionId ? { sessionId: row.primarySessionId } : {},
|
|
1664
|
+
displayName: row.primaryDisplayName ?? (row.primaryKind === "workspace" ? "Workspace" : "Untitled session"),
|
|
1665
|
+
displayNameOriginalChars: Number(
|
|
1666
|
+
row.primaryDisplayNameOriginalChars ?? (row.primaryKind === "workspace" ? 9 : "Untitled session".length)
|
|
1667
|
+
)
|
|
1668
|
+
} : null;
|
|
1669
|
+
if (blockerCount > 0 !== (primaryBlocker !== null)) {
|
|
1670
|
+
throw new SessionControlInvariantError(
|
|
1671
|
+
`Session ${row.targetId} discovery blocker aggregate is inconsistent`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
result.set(row.targetId, {
|
|
1675
|
+
state: blockerCount > 0 ? "paused" : "active",
|
|
1676
|
+
primaryBlocker,
|
|
1677
|
+
additionalBlockerCount: Math.max(0, blockerCount - 1)
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
if (result.size !== uniqueIds.length) {
|
|
1681
|
+
throw new SessionControlInvariantError("Session discovery control projection is incomplete");
|
|
1682
|
+
}
|
|
1683
|
+
return result;
|
|
1684
|
+
}
|
|
1685
|
+
async function evaluateSessionControl(db, workspaceId, sessionId, options = {}) {
|
|
1686
|
+
return (await evaluateSessionControls(db, workspaceId, [sessionId], options)).get(sessionId);
|
|
1687
|
+
}
|
|
1688
|
+
async function findCommandReceipt(db, input) {
|
|
1689
|
+
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
1690
|
+
const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
|
|
1691
|
+
const identity = input.identityScope === "target_operation" ? and3(
|
|
1692
|
+
eq3(sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
1693
|
+
eq3(sessionCommandReceipts.actorType, "agent_attempt"),
|
|
1694
|
+
eq3(sessionCommandReceipts.action, input.action),
|
|
1695
|
+
eq3(sessionCommandReceipts.targetSessionId, input.targetSessionId),
|
|
1696
|
+
sql3`${sessionCommandReceipts.targetTurnId} is null`,
|
|
1697
|
+
eq3(sessionCommandReceipts.operationKey, input.operationKey)
|
|
1698
|
+
) : and3(
|
|
1699
|
+
eq3(sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
1700
|
+
eq3(sessionCommandReceipts.actorType, input.actor.type),
|
|
1701
|
+
sql3`${sessionCommandReceipts.actorSubjectId} is not distinct from ${actorSubjectId}`,
|
|
1702
|
+
sql3`${sessionCommandReceipts.actorAttemptId} is not distinct from ${actorAttemptId}::uuid`,
|
|
1703
|
+
eq3(sessionCommandReceipts.action, input.action),
|
|
1704
|
+
sql3`${sessionCommandReceipts.targetSessionId} is not distinct from ${input.targetSessionId}::uuid`,
|
|
1705
|
+
sql3`${sessionCommandReceipts.targetTurnId} is not distinct from ${input.targetTurnId}::uuid`,
|
|
1706
|
+
eq3(sessionCommandReceipts.operationKey, input.operationKey)
|
|
1707
|
+
);
|
|
1708
|
+
const rows = await db.select().from(sessionCommandReceipts).where(identity).for("update");
|
|
1709
|
+
return rows[0] ?? null;
|
|
1710
|
+
}
|
|
1711
|
+
async function findPromptCommandReceipts(db, input) {
|
|
1712
|
+
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
1713
|
+
const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
|
|
1714
|
+
const query = db.select().from(sessionCommandReceipts).where(
|
|
1715
|
+
and3(
|
|
1716
|
+
eq3(sessionCommandReceipts.workspaceId, input.workspaceId),
|
|
1717
|
+
eq3(sessionCommandReceipts.actorType, input.actor.type),
|
|
1718
|
+
sql3`${sessionCommandReceipts.actorSubjectId} is not distinct from ${actorSubjectId}`,
|
|
1719
|
+
sql3`${sessionCommandReceipts.actorAttemptId} is not distinct from ${actorAttemptId}::uuid`,
|
|
1720
|
+
inArray2(sessionCommandReceipts.action, ["prompt.send", "prompt.steer"]),
|
|
1721
|
+
eq3(sessionCommandReceipts.operationKey, input.operationKey)
|
|
1722
|
+
)
|
|
1723
|
+
);
|
|
1724
|
+
const rows = input.lock === "update" ? await query.for("update") : await query;
|
|
1725
|
+
return rows;
|
|
1726
|
+
}
|
|
1727
|
+
function fingerprintedPromptCommandReceipt(receipts) {
|
|
1728
|
+
const fingerprinted = receipts.filter(
|
|
1729
|
+
(receipt) => typeof receipt.result.boundaryRequestHash === "string"
|
|
1730
|
+
);
|
|
1731
|
+
if (fingerprinted.length > 1) {
|
|
1732
|
+
throw new SessionControlInvariantError(
|
|
1733
|
+
"Prompt operation key resolved to multiple fingerprinted receipts"
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
return fingerprinted[0] ?? null;
|
|
1737
|
+
}
|
|
1738
|
+
function assertPromptCommandReceiptIdentity(receipt, input) {
|
|
1739
|
+
if (receipt.action !== input.action || receipt.targetSessionId !== input.targetSessionId || receipt.targetTurnId !== null || typeof receipt.result.boundaryRequestHash === "string" && receipt.result.boundaryRequestHash !== input.boundaryRequestHash) {
|
|
1740
|
+
throw new SessionCommandIdempotencyError();
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async function reserveSessionPromptCommandReceipt(db, input) {
|
|
1744
|
+
await lockSessionPromptCommandOperation(db, input);
|
|
1745
|
+
const existingReceipts = await findPromptCommandReceipts(db, {
|
|
1746
|
+
workspaceId: input.workspaceId,
|
|
1747
|
+
actor: input.actor,
|
|
1748
|
+
operationKey: input.operationKey,
|
|
1749
|
+
lock: "update"
|
|
1750
|
+
});
|
|
1751
|
+
const fingerprinted = fingerprintedPromptCommandReceipt(existingReceipts);
|
|
1752
|
+
if (fingerprinted) {
|
|
1753
|
+
assertPromptCommandReceiptIdentity(fingerprinted, input);
|
|
1754
|
+
return { receipt: fingerprinted, replay: true };
|
|
1755
|
+
}
|
|
1756
|
+
const legacyExact = existingReceipts.find(
|
|
1757
|
+
(receipt) => receipt.action === input.action && receipt.targetSessionId === input.targetSessionId && receipt.targetTurnId === null
|
|
1758
|
+
);
|
|
1759
|
+
if (legacyExact) {
|
|
1760
|
+
if (legacyExact.canonicalRequestHash !== input.canonicalRequestHash) {
|
|
1761
|
+
throw new SessionCommandIdempotencyError();
|
|
1762
|
+
}
|
|
1763
|
+
return { receipt: legacyExact, replay: true };
|
|
1764
|
+
}
|
|
1765
|
+
return await reserveSessionCommandReceipt(db, {
|
|
1766
|
+
accountId: input.accountId,
|
|
1767
|
+
workspaceId: input.workspaceId,
|
|
1768
|
+
actor: input.actor,
|
|
1769
|
+
action: input.action,
|
|
1770
|
+
targetSessionId: input.targetSessionId,
|
|
1771
|
+
targetTurnId: null,
|
|
1772
|
+
operationKey: input.operationKey,
|
|
1773
|
+
canonicalRequestHash: input.canonicalRequestHash,
|
|
1774
|
+
...existingReceipts.length === 0 ? { initialResult: { boundaryRequestHash: input.boundaryRequestHash } } : {}
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
async function lockSessionPromptCommandOperation(db, input) {
|
|
1778
|
+
const actorIdentity = input.actor.type === "agent_attempt" ? `attempt:${input.actor.attemptId}` : `${input.actor.type}:${input.actor.subjectId}`;
|
|
1779
|
+
await db.execute(
|
|
1780
|
+
sql3`select pg_advisory_xact_lock(hashtextextended(${`session-prompt:${input.workspaceId}:${actorIdentity}:${input.operationKey}`}, 0))`
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
async function getCompletedSessionPromptCommandReceipt(db, input) {
|
|
1784
|
+
const receipt = fingerprintedPromptCommandReceipt(
|
|
1785
|
+
await findPromptCommandReceipts(db, {
|
|
1786
|
+
workspaceId: input.workspaceId,
|
|
1787
|
+
actor: input.actor,
|
|
1788
|
+
operationKey: input.operationKey
|
|
1789
|
+
})
|
|
1790
|
+
);
|
|
1791
|
+
if (!receipt) return null;
|
|
1792
|
+
assertPromptCommandReceiptIdentity(receipt, input);
|
|
1793
|
+
if (typeof receipt.result.boundaryRequestHash !== "string" || receipt.appliedQueueVersion === null) {
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
return receipt;
|
|
1797
|
+
}
|
|
1798
|
+
async function reserveSessionCommandReceipt(db, input) {
|
|
1799
|
+
if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
|
|
1800
|
+
const identityScope = input.identityScope ?? "actor";
|
|
1801
|
+
if (identityScope === "target_operation" && (input.actor.type !== "agent_attempt" || !["goal.update", "goal.progress", "session.wait_for_input"].includes(input.action) || input.targetSessionId === null || input.targetTurnId !== null)) {
|
|
1802
|
+
throw new SessionControlInvariantError(
|
|
1803
|
+
"Target-scoped receipt identity is reserved for agent self commands"
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const actorSubjectId = input.actor.type === "agent_attempt" ? null : input.actor.subjectId;
|
|
1807
|
+
const actorAttemptId = input.actor.type === "agent_attempt" ? input.actor.attemptId : null;
|
|
1808
|
+
const [inserted] = await db.insert(sessionCommandReceipts).values({
|
|
1809
|
+
accountId: input.accountId,
|
|
1810
|
+
workspaceId: input.workspaceId,
|
|
1811
|
+
actorType: input.actor.type,
|
|
1812
|
+
actorSubjectId,
|
|
1813
|
+
actorAttemptId,
|
|
1814
|
+
action: input.action,
|
|
1815
|
+
targetSessionId: input.targetSessionId,
|
|
1816
|
+
targetTurnId: input.targetTurnId,
|
|
1817
|
+
operationKey: input.operationKey,
|
|
1818
|
+
canonicalRequestHash: input.canonicalRequestHash,
|
|
1819
|
+
result: input.initialResult ?? {}
|
|
1820
|
+
}).onConflictDoNothing().returning();
|
|
1821
|
+
const receipt = inserted ?? await findCommandReceipt(db, {
|
|
1822
|
+
workspaceId: input.workspaceId,
|
|
1823
|
+
actor: input.actor,
|
|
1824
|
+
action: input.action,
|
|
1825
|
+
targetSessionId: input.targetSessionId,
|
|
1826
|
+
targetTurnId: input.targetTurnId,
|
|
1827
|
+
operationKey: input.operationKey,
|
|
1828
|
+
identityScope
|
|
1829
|
+
});
|
|
1830
|
+
if (!receipt) throw new SessionControlInvariantError("Command receipt conflict was not readable");
|
|
1831
|
+
if (receipt.canonicalRequestHash !== input.canonicalRequestHash) {
|
|
1832
|
+
throw new SessionCommandIdempotencyError();
|
|
1833
|
+
}
|
|
1834
|
+
return { receipt, replay: !inserted };
|
|
1835
|
+
}
|
|
1836
|
+
function nextRevision(workspace) {
|
|
1837
|
+
const current = asSafeRevision(workspace.revision, "workspace control revision");
|
|
1838
|
+
if (current >= Number.MAX_SAFE_INTEGER) {
|
|
1839
|
+
throw new SessionControlInvariantError("Workspace control revision is exhausted");
|
|
1840
|
+
}
|
|
1841
|
+
return current + 1;
|
|
1842
|
+
}
|
|
1843
|
+
async function advanceWorkspaceRevision(db, workspaceId, revision) {
|
|
1844
|
+
const rows = await db.update(workspaceInferenceControls).set({ revision, updatedAt: /* @__PURE__ */ new Date() }).where(
|
|
1845
|
+
and3(
|
|
1846
|
+
eq3(workspaceInferenceControls.workspaceId, workspaceId),
|
|
1847
|
+
eq3(workspaceInferenceControls.revision, revision - 1)
|
|
1848
|
+
)
|
|
1849
|
+
).returning({ workspaceId: workspaceInferenceControls.workspaceId });
|
|
1850
|
+
if (rows.length !== 1) {
|
|
1851
|
+
throw new SessionControlInvariantError(
|
|
1852
|
+
"Workspace control revision did not advance exactly once"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async function registerContinuableWakes(db, input) {
|
|
1857
|
+
const rows = await db.execute(sql3`
|
|
1858
|
+
with eligible as (
|
|
1859
|
+
select *
|
|
1860
|
+
from opengeni_private.list_continuable_sessions(
|
|
1861
|
+
${input.workspaceId}::uuid,
|
|
1862
|
+
${input.rootSessionId}::uuid
|
|
1863
|
+
)
|
|
1864
|
+
), upserted as (
|
|
1865
|
+
insert into ${sessionWorkflowWakeOutbox} (
|
|
1866
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1867
|
+
)
|
|
1868
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
|
|
1869
|
+
from eligible
|
|
1870
|
+
on conflict (session_id) do update set
|
|
1871
|
+
wake_revision = ${sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1872
|
+
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1873
|
+
reason = excluded.reason,
|
|
1874
|
+
attempts = 0,
|
|
1875
|
+
next_attempt_at = now(),
|
|
1876
|
+
last_error = null,
|
|
1877
|
+
updated_at = now()
|
|
1878
|
+
returning session_id
|
|
1879
|
+
)
|
|
1880
|
+
select count(*)::bigint as "wakeCount" from upserted
|
|
1881
|
+
`);
|
|
1882
|
+
return Number(rows[0]?.wakeCount ?? 0);
|
|
1883
|
+
}
|
|
1884
|
+
async function registerDescendantWakes(db, input) {
|
|
1885
|
+
return await registerContinuableWakes(db, {
|
|
1886
|
+
workspaceId: input.workspaceId,
|
|
1887
|
+
rootSessionId: input.sessionId,
|
|
1888
|
+
reason: input.reason
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
async function registerWorkspaceWakes(db, input) {
|
|
1892
|
+
return await registerContinuableWakes(db, {
|
|
1893
|
+
workspaceId: input.workspaceId,
|
|
1894
|
+
rootSessionId: null,
|
|
1895
|
+
reason: input.reason
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
async function registerInterruptionWakes(db, input) {
|
|
1899
|
+
const rows = await db.execute(sql3`
|
|
1900
|
+
with eligible as (
|
|
1901
|
+
select distinct
|
|
1902
|
+
session.id as session_id,
|
|
1903
|
+
session.account_id,
|
|
1904
|
+
session.workspace_id,
|
|
1905
|
+
coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
|
|
1906
|
+
as temporal_workflow_id
|
|
1907
|
+
from ${sessionAttemptInterruptions} interruption
|
|
1908
|
+
join ${sessions} session
|
|
1909
|
+
on session.workspace_id = interruption.workspace_id
|
|
1910
|
+
and session.id = interruption.session_id
|
|
1911
|
+
where interruption.operation_id = ${input.operationId}::uuid
|
|
1912
|
+
and interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
1913
|
+
), upserted as (
|
|
1914
|
+
insert into ${sessionWorkflowWakeOutbox} (
|
|
1915
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1916
|
+
)
|
|
1917
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, ${input.reason}
|
|
1918
|
+
from eligible
|
|
1919
|
+
on conflict (session_id) do update set
|
|
1920
|
+
wake_revision = ${sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1921
|
+
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1922
|
+
reason = excluded.reason,
|
|
1923
|
+
attempts = 0,
|
|
1924
|
+
next_attempt_at = now(),
|
|
1925
|
+
last_error = null,
|
|
1926
|
+
updated_at = now()
|
|
1927
|
+
returning session_id
|
|
1928
|
+
)
|
|
1929
|
+
select count(*)::bigint as "wakeCount" from upserted
|
|
1930
|
+
`);
|
|
1931
|
+
return Number(rows[0]?.wakeCount ?? 0);
|
|
1932
|
+
}
|
|
1933
|
+
async function registerCancellationWakes(db, input) {
|
|
1934
|
+
if (input.sessionIds.length === 0) return 0;
|
|
1935
|
+
const sessionIds = sql3.join(
|
|
1936
|
+
input.sessionIds.map((id) => sql3`${id}::uuid`),
|
|
1937
|
+
sql3`, `
|
|
1938
|
+
);
|
|
1939
|
+
const rows = await db.execute(sql3`
|
|
1940
|
+
with eligible as (
|
|
1941
|
+
select
|
|
1942
|
+
session.id as session_id,
|
|
1943
|
+
session.account_id,
|
|
1944
|
+
session.workspace_id,
|
|
1945
|
+
coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
|
|
1946
|
+
as temporal_workflow_id
|
|
1947
|
+
from ${sessions} session
|
|
1948
|
+
where session.workspace_id = ${input.workspaceId}
|
|
1949
|
+
and session.id in (${sessionIds})
|
|
1950
|
+
), upserted as (
|
|
1951
|
+
insert into ${sessionWorkflowWakeOutbox} (
|
|
1952
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason, control_revision
|
|
1953
|
+
)
|
|
1954
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, 'session_cancelled', 1
|
|
1955
|
+
from eligible
|
|
1956
|
+
on conflict (session_id) do update set
|
|
1957
|
+
wake_revision = ${sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1958
|
+
control_revision = ${sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1959
|
+
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1960
|
+
reason = excluded.reason,
|
|
1961
|
+
attempts = 0,
|
|
1962
|
+
next_attempt_at = now(),
|
|
1963
|
+
last_error = null,
|
|
1964
|
+
updated_at = now()
|
|
1965
|
+
returning session_id
|
|
1966
|
+
)
|
|
1967
|
+
select count(*)::bigint as "wakeCount" from upserted
|
|
1968
|
+
`);
|
|
1969
|
+
return Number(rows[0]?.wakeCount ?? 0);
|
|
1970
|
+
}
|
|
1971
|
+
async function registerSessionWorkflowWakeInTransaction(db, input) {
|
|
1972
|
+
const [row] = await db.insert(sessionWorkflowWakeOutbox).values({
|
|
1973
|
+
accountId: input.accountId,
|
|
1974
|
+
workspaceId: input.workspaceId,
|
|
1975
|
+
sessionId: input.sessionId,
|
|
1976
|
+
temporalWorkflowId: input.temporalWorkflowId,
|
|
1977
|
+
controlRevision: input.controlRequested ? 1 : 0,
|
|
1978
|
+
reason: input.reason
|
|
1979
|
+
}).onConflictDoUpdate({
|
|
1980
|
+
target: sessionWorkflowWakeOutbox.sessionId,
|
|
1981
|
+
set: {
|
|
1982
|
+
temporalWorkflowId: input.temporalWorkflowId,
|
|
1983
|
+
wakeRevision: sql3`${sessionWorkflowWakeOutbox.wakeRevision} + 1`,
|
|
1984
|
+
controlRevision: input.controlRequested ? sql3`${sessionWorkflowWakeOutbox.wakeRevision} + 1` : sql3`${sessionWorkflowWakeOutbox.controlRevision}`,
|
|
1985
|
+
reason: input.reason,
|
|
1986
|
+
attempts: 0,
|
|
1987
|
+
nextAttemptAt: /* @__PURE__ */ new Date(),
|
|
1988
|
+
lastError: null,
|
|
1989
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1990
|
+
}
|
|
1991
|
+
}).returning({ wakeRevision: sessionWorkflowWakeOutbox.wakeRevision });
|
|
1992
|
+
if (!row) {
|
|
1993
|
+
throw new SessionControlInvariantError(`Failed to register wake for ${input.sessionId}`);
|
|
1994
|
+
}
|
|
1995
|
+
return Number(row.wakeRevision);
|
|
1996
|
+
}
|
|
1997
|
+
async function registerInternalUpdateWakeInTransaction(db, input) {
|
|
1998
|
+
const [existing] = await db.select().from(sessionWorkflowWakeOutbox).where(
|
|
1999
|
+
and3(
|
|
2000
|
+
eq3(sessionWorkflowWakeOutbox.workspaceId, input.workspaceId),
|
|
2001
|
+
eq3(sessionWorkflowWakeOutbox.sessionId, input.sessionId)
|
|
2002
|
+
)
|
|
2003
|
+
).for("update").limit(1);
|
|
2004
|
+
if (existing && existing.wakeRevision > existing.deliveredRevision) {
|
|
2005
|
+
await db.update(sessionWorkflowWakeOutbox).set({
|
|
2006
|
+
temporalWorkflowId: input.temporalWorkflowId,
|
|
2007
|
+
reason: "internal_update_batch",
|
|
2008
|
+
nextAttemptAt: /* @__PURE__ */ new Date(),
|
|
2009
|
+
lastError: null,
|
|
2010
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2011
|
+
}).where(eq3(sessionWorkflowWakeOutbox.sessionId, input.sessionId));
|
|
2012
|
+
return { wakeRevision: existing.wakeRevision, shouldSignal: false };
|
|
2013
|
+
}
|
|
2014
|
+
return {
|
|
2015
|
+
wakeRevision: await registerSessionWorkflowWakeInTransaction(db, {
|
|
2016
|
+
...input,
|
|
2017
|
+
reason: "internal_update_batch"
|
|
2018
|
+
}),
|
|
2019
|
+
shouldSignal: true
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
async function interruptDescendantAttempts(db, input) {
|
|
2023
|
+
const rows = await db.execute(sql3`
|
|
2024
|
+
with recursive live_attempts as (
|
|
2025
|
+
select attempt.id, attempt.account_id, attempt.workspace_id, attempt.session_id
|
|
2026
|
+
from ${sessionTurnAttempts} attempt
|
|
2027
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
2028
|
+
and attempt.state in ('claimed', 'running')
|
|
2029
|
+
), attempt_ancestry(attempt_id, account_id, workspace_id, session_id, ancestor_id, depth, path) as (
|
|
2030
|
+
select
|
|
2031
|
+
attempt.id,
|
|
2032
|
+
attempt.account_id,
|
|
2033
|
+
attempt.workspace_id,
|
|
2034
|
+
attempt.session_id,
|
|
2035
|
+
attempt.session_id,
|
|
2036
|
+
0::integer,
|
|
2037
|
+
array[attempt.session_id]::uuid[]
|
|
2038
|
+
from live_attempts attempt
|
|
2039
|
+
union all
|
|
2040
|
+
select
|
|
2041
|
+
ancestry.attempt_id,
|
|
2042
|
+
ancestry.account_id,
|
|
2043
|
+
ancestry.workspace_id,
|
|
2044
|
+
ancestry.session_id,
|
|
2045
|
+
current.parent_session_id,
|
|
2046
|
+
ancestry.depth + 1,
|
|
2047
|
+
ancestry.path || current.parent_session_id
|
|
2048
|
+
from attempt_ancestry ancestry
|
|
2049
|
+
join ${sessions} current
|
|
2050
|
+
on current.workspace_id = ${input.workspaceId} and current.id = ancestry.ancestor_id
|
|
2051
|
+
where current.parent_session_id is not null
|
|
2052
|
+
and not current.parent_session_id = any(ancestry.path)
|
|
2053
|
+
and ancestry.depth < ${SESSION_ANCESTRY_LIMIT}
|
|
2054
|
+
), inserted as (
|
|
2055
|
+
insert into ${sessionAttemptInterruptions} (
|
|
2056
|
+
account_id, workspace_id, session_id, operation_id, attempt_id,
|
|
2057
|
+
kind, control_revision
|
|
2058
|
+
)
|
|
2059
|
+
select ancestry.account_id, ancestry.workspace_id, ancestry.session_id,
|
|
2060
|
+
${input.operationId}::uuid, ancestry.attempt_id, ${input.kind}, ${input.controlRevision}
|
|
2061
|
+
from attempt_ancestry ancestry
|
|
2062
|
+
where ancestry.ancestor_id = ${input.sessionId}
|
|
2063
|
+
on conflict (operation_id, attempt_id) do nothing
|
|
2064
|
+
returning id
|
|
2065
|
+
)
|
|
2066
|
+
select count(*)::integer as count from inserted
|
|
2067
|
+
`);
|
|
2068
|
+
return Number(rows[0]?.count ?? 0);
|
|
2069
|
+
}
|
|
2070
|
+
async function interruptWorkspaceAttempts(db, input) {
|
|
2071
|
+
const rows = await db.execute(sql3`
|
|
2072
|
+
with inserted as (
|
|
2073
|
+
insert into ${sessionAttemptInterruptions} (
|
|
2074
|
+
account_id, workspace_id, session_id, operation_id, attempt_id,
|
|
2075
|
+
kind, control_revision
|
|
2076
|
+
)
|
|
2077
|
+
select attempt.account_id, attempt.workspace_id, attempt.session_id,
|
|
2078
|
+
${input.operationId}::uuid, attempt.id, 'workspace_pause',
|
|
2079
|
+
${input.controlRevision}
|
|
2080
|
+
from ${sessionTurnAttempts} attempt
|
|
2081
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
2082
|
+
and attempt.state in ('claimed', 'running')
|
|
2083
|
+
on conflict (operation_id, attempt_id) do nothing
|
|
2084
|
+
returning id
|
|
2085
|
+
)
|
|
2086
|
+
select count(*)::integer as count from inserted
|
|
2087
|
+
`);
|
|
2088
|
+
return Number(rows[0]?.count ?? 0);
|
|
2089
|
+
}
|
|
2090
|
+
async function updateSessionCommandReceiptResult(db, receiptId, input) {
|
|
2091
|
+
const [receipt] = await db.update(sessionCommandReceipts).set({
|
|
2092
|
+
...input.controlRevision !== void 0 ? { appliedControlRevision: input.controlRevision } : {},
|
|
2093
|
+
...input.queueVersion !== void 0 ? { appliedQueueVersion: input.queueVersion } : {},
|
|
2094
|
+
...input.turnVersion !== void 0 ? { appliedTurnVersion: input.turnVersion } : {},
|
|
2095
|
+
...input.draftRevision !== void 0 ? { appliedDraftRevision: input.draftRevision } : {},
|
|
2096
|
+
result: input.result,
|
|
2097
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2098
|
+
}).where(eq3(sessionCommandReceipts.id, receiptId)).returning();
|
|
2099
|
+
if (!receipt) throw new SessionControlInvariantError("Command receipt disappeared");
|
|
2100
|
+
return receipt;
|
|
2101
|
+
}
|
|
2102
|
+
async function pendingSessionControlWorkflowWake(db, workspaceId, sessionId, wakeCount) {
|
|
2103
|
+
if (wakeCount === 0) return null;
|
|
2104
|
+
const [wake] = await db.select({
|
|
2105
|
+
accountId: sessionWorkflowWakeOutbox.accountId,
|
|
2106
|
+
workspaceId: sessionWorkflowWakeOutbox.workspaceId,
|
|
2107
|
+
sessionId: sessionWorkflowWakeOutbox.sessionId,
|
|
2108
|
+
temporalWorkflowId: sessionWorkflowWakeOutbox.temporalWorkflowId,
|
|
2109
|
+
wakeRevision: sessionWorkflowWakeOutbox.wakeRevision,
|
|
2110
|
+
deliveredRevision: sessionWorkflowWakeOutbox.deliveredRevision
|
|
2111
|
+
}).from(sessionWorkflowWakeOutbox).where(
|
|
2112
|
+
and3(
|
|
2113
|
+
eq3(sessionWorkflowWakeOutbox.workspaceId, workspaceId),
|
|
2114
|
+
eq3(sessionWorkflowWakeOutbox.sessionId, sessionId)
|
|
2115
|
+
)
|
|
2116
|
+
).limit(1);
|
|
2117
|
+
if (!wake || wake.wakeRevision <= wake.deliveredRevision) return null;
|
|
2118
|
+
return {
|
|
2119
|
+
accountId: wake.accountId,
|
|
2120
|
+
workspaceId: wake.workspaceId,
|
|
2121
|
+
sessionId: wake.sessionId,
|
|
2122
|
+
temporalWorkflowId: wake.temporalWorkflowId,
|
|
2123
|
+
wakeRevision: wake.wakeRevision
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
async function sessionPauseEffectsNeeded(db, input) {
|
|
2127
|
+
const rows = await db.execute(sql3`
|
|
2128
|
+
with recursive subtree(id, path, cycle) as (
|
|
2129
|
+
select session.id, array[session.id]::uuid[], false
|
|
2130
|
+
from ${sessions} session
|
|
2131
|
+
where session.workspace_id = ${input.workspaceId}
|
|
2132
|
+
and session.id = ${input.sessionId}
|
|
2133
|
+
union all
|
|
2134
|
+
select child.id, parent.path || child.id, child.id = any(parent.path)
|
|
2135
|
+
from subtree parent
|
|
2136
|
+
join ${sessions} child
|
|
2137
|
+
on child.workspace_id = ${input.workspaceId}
|
|
2138
|
+
and child.parent_session_id = parent.id
|
|
2139
|
+
where not parent.cycle
|
|
2140
|
+
and cardinality(parent.path) <= ${SESSION_ANCESTRY_LIMIT}
|
|
2141
|
+
), scoped_sessions as (
|
|
2142
|
+
select session.id, session.subtree_run_override_revision
|
|
2143
|
+
from subtree
|
|
2144
|
+
join ${sessions} session
|
|
2145
|
+
on session.workspace_id = ${input.workspaceId}
|
|
2146
|
+
and session.id = subtree.id
|
|
2147
|
+
where not subtree.cycle
|
|
2148
|
+
)
|
|
2149
|
+
select exists (
|
|
2150
|
+
select 1
|
|
2151
|
+
from scoped_sessions session
|
|
2152
|
+
where session.subtree_run_override_revision > ${input.directPauseRevision}
|
|
2153
|
+
union all
|
|
2154
|
+
select 1
|
|
2155
|
+
from ${sessionTurnAttempts} attempt
|
|
2156
|
+
join scoped_sessions session on session.id = attempt.session_id
|
|
2157
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
2158
|
+
and attempt.state in ('claimed', 'running')
|
|
2159
|
+
and not exists (
|
|
2160
|
+
select 1
|
|
2161
|
+
from ${sessionAttemptInterruptions} interruption
|
|
2162
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
2163
|
+
and interruption.attempt_id = attempt.id
|
|
2164
|
+
and interruption.kind in ('session_pause', 'workspace_pause')
|
|
2165
|
+
and interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
2166
|
+
)
|
|
2167
|
+
union all
|
|
2168
|
+
select 1
|
|
2169
|
+
from ${sessionBackgroundCommands} command
|
|
2170
|
+
join scoped_sessions session on session.id = command.session_id
|
|
2171
|
+
where command.workspace_id = ${input.workspaceId}
|
|
2172
|
+
and command.state = 'running'
|
|
2173
|
+
) as needed
|
|
2174
|
+
`);
|
|
2175
|
+
return rows[0]?.needed ?? false;
|
|
2176
|
+
}
|
|
2177
|
+
async function workspacePauseEffectsNeeded(db, input) {
|
|
2178
|
+
const rows = await db.execute(sql3`
|
|
2179
|
+
select exists (
|
|
2180
|
+
select 1
|
|
2181
|
+
from ${sessions} session
|
|
2182
|
+
where session.workspace_id = ${input.workspaceId}
|
|
2183
|
+
and session.subtree_run_override_revision > ${input.workspacePauseRevision}
|
|
2184
|
+
union all
|
|
2185
|
+
select 1
|
|
2186
|
+
from ${sessionTurnAttempts} attempt
|
|
2187
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
2188
|
+
and attempt.state in ('claimed', 'running')
|
|
2189
|
+
and not exists (
|
|
2190
|
+
select 1
|
|
2191
|
+
from ${sessionAttemptInterruptions} interruption
|
|
2192
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
2193
|
+
and interruption.attempt_id = attempt.id
|
|
2194
|
+
and interruption.kind in ('session_pause', 'workspace_pause')
|
|
2195
|
+
and interruption.state in ('pending', 'delivered', 'acknowledged')
|
|
2196
|
+
)
|
|
2197
|
+
union all
|
|
2198
|
+
select 1
|
|
2199
|
+
from ${sessionBackgroundCommands} command
|
|
2200
|
+
where command.workspace_id = ${input.workspaceId}
|
|
2201
|
+
and command.state = 'running'
|
|
2202
|
+
) as needed
|
|
2203
|
+
`);
|
|
2204
|
+
return rows[0]?.needed ?? false;
|
|
2205
|
+
}
|
|
2206
|
+
async function continuableWakeRepairNeeded(db, input) {
|
|
2207
|
+
const rows = await db.execute(sql3`
|
|
2208
|
+
select exists (
|
|
2209
|
+
select 1
|
|
2210
|
+
from opengeni_private.list_continuable_sessions(
|
|
2211
|
+
${input.workspaceId}::uuid,
|
|
2212
|
+
${input.rootSessionId}::uuid
|
|
2213
|
+
) eligible
|
|
2214
|
+
left join ${sessionWorkflowWakeOutbox} wake
|
|
2215
|
+
on wake.workspace_id = eligible.workspace_id
|
|
2216
|
+
and wake.session_id = eligible.session_id
|
|
2217
|
+
where wake.session_id is null
|
|
2218
|
+
or wake.wake_revision <= wake.delivered_revision
|
|
2219
|
+
) as needed
|
|
2220
|
+
`);
|
|
2221
|
+
return rows[0]?.needed ?? false;
|
|
2222
|
+
}
|
|
2223
|
+
async function loadSessionSubtreeIds(db, workspaceId, rootSessionId) {
|
|
2224
|
+
const rows = await db.execute(sql3`
|
|
2225
|
+
with recursive subtree(id, root_parent_session_id, depth, path, cycle) as (
|
|
2226
|
+
select session.id, session.parent_session_id, 0::integer, array[session.id]::uuid[], false
|
|
2227
|
+
from ${sessions} session
|
|
2228
|
+
where session.workspace_id = ${workspaceId} and session.id = ${rootSessionId}
|
|
2229
|
+
union all
|
|
2230
|
+
select child.id, parent.root_parent_session_id, parent.depth + 1,
|
|
2231
|
+
parent.path || child.id, child.id = any(parent.path)
|
|
2232
|
+
from subtree parent
|
|
2233
|
+
join ${sessions} child
|
|
2234
|
+
on child.workspace_id = ${workspaceId} and child.parent_session_id = parent.id
|
|
2235
|
+
where parent.depth < ${SESSION_ANCESTRY_LIMIT} and not parent.cycle
|
|
2236
|
+
)
|
|
2237
|
+
select id, root_parent_session_id as "rootParentSessionId", depth, cycle
|
|
2238
|
+
from subtree
|
|
2239
|
+
order by id
|
|
2240
|
+
`);
|
|
2241
|
+
if (rows.length === 0) {
|
|
2242
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} does not exist`);
|
|
2243
|
+
}
|
|
2244
|
+
if (rows.some((row) => row.cycle || Number(row.depth) >= SESSION_ANCESTRY_LIMIT)) {
|
|
2245
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} subtree is invalid`);
|
|
2246
|
+
}
|
|
2247
|
+
return {
|
|
2248
|
+
sessionIds: rows.map((row) => row.id),
|
|
2249
|
+
rootParentSessionId: rows[0]?.rootParentSessionId ?? null
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
var TERMINAL_CANCELLATION_LIVE_TURN_STATUSES = [
|
|
2253
|
+
"queued",
|
|
2254
|
+
"running",
|
|
2255
|
+
"requires_action",
|
|
2256
|
+
"recovering",
|
|
2257
|
+
"waiting_capacity"
|
|
2258
|
+
];
|
|
2259
|
+
async function loadCancellationTurnIds(db, workspaceId, sessionIds) {
|
|
2260
|
+
const rows = await db.select({ id: sessionTurns.id }).from(sessionTurns).where(
|
|
2261
|
+
and3(
|
|
2262
|
+
eq3(sessionTurns.workspaceId, workspaceId),
|
|
2263
|
+
inArray2(sessionTurns.sessionId, sessionIds),
|
|
2264
|
+
inArray2(sessionTurns.status, TERMINAL_CANCELLATION_LIVE_TURN_STATUSES)
|
|
2265
|
+
)
|
|
2266
|
+
).orderBy(sessionTurns.id);
|
|
2267
|
+
return rows.map((row) => row.id);
|
|
2268
|
+
}
|
|
2269
|
+
async function loadParentSessionId(db, workspaceId, sessionId) {
|
|
2270
|
+
const [row] = await db.select({ parentSessionId: sessions.parentSessionId }).from(sessions).where(and3(eq3(sessions.workspaceId, workspaceId), eq3(sessions.id, sessionId))).limit(1);
|
|
2271
|
+
return row?.parentSessionId ?? null;
|
|
2272
|
+
}
|
|
2273
|
+
async function insertChildOutboxRowInTransaction(db, input) {
|
|
2274
|
+
const parentSessionId = input.childSession.parentSessionId;
|
|
2275
|
+
if (!parentSessionId) return;
|
|
2276
|
+
if (!input.parentSession || input.parentSession.id !== parentSessionId) {
|
|
2277
|
+
throw new SessionControlInvariantError(
|
|
2278
|
+
`Parent session ${parentSessionId} was not locked with child ${input.childSession.id}`
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
if (input.parentSession.status === "cancelled") return;
|
|
2282
|
+
if (input.payload.type !== input.kind) {
|
|
2283
|
+
throw new SessionControlInvariantError(
|
|
2284
|
+
`Child outbox ${input.kind} payload discriminator mismatch`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
let personalConnectionDelegations = [];
|
|
2288
|
+
if (input.childSession.parentTurnId) {
|
|
2289
|
+
const [parentTurn] = await db.select({
|
|
2290
|
+
delegations: sessionTurns.personalConnectionDelegations
|
|
2291
|
+
}).from(sessionTurns).where(
|
|
2292
|
+
and3(
|
|
2293
|
+
eq3(sessionTurns.workspaceId, input.workspaceId),
|
|
2294
|
+
eq3(sessionTurns.sessionId, parentSessionId),
|
|
2295
|
+
eq3(sessionTurns.id, input.childSession.parentTurnId)
|
|
2296
|
+
)
|
|
2297
|
+
).limit(1);
|
|
2298
|
+
if (parentTurn) {
|
|
2299
|
+
const parsed = McpPersonalConnectionDelegations.safeParse(parentTurn.delegations);
|
|
2300
|
+
if (!parsed.success) {
|
|
2301
|
+
throw new SessionControlInvariantError(
|
|
2302
|
+
`Invalid personal MCP delegation snapshot at session_turns:${input.workspaceId}:${parentSessionId}:${input.childSession.parentTurnId}`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
personalConnectionDelegations = parsed.data.map((delegation) => ({
|
|
2306
|
+
...delegation
|
|
2307
|
+
}));
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
await db.insert(sessionSystemUpdateOutbox).values(
|
|
2311
|
+
withLosslessContentWriteVersion(
|
|
2312
|
+
withLosslessContentWriteVersion(
|
|
2313
|
+
{
|
|
2314
|
+
accountId: input.childSession.accountId,
|
|
2315
|
+
workspaceId: input.workspaceId,
|
|
2316
|
+
sourceSessionId: input.childSession.id,
|
|
2317
|
+
targetSessionId: parentSessionId,
|
|
2318
|
+
dedupeKey: input.dedupeKey,
|
|
2319
|
+
kind: input.kind,
|
|
2320
|
+
classification: input.classification,
|
|
2321
|
+
sourceId: input.childSession.id,
|
|
2322
|
+
summary: input.summary,
|
|
2323
|
+
payload: input.payload,
|
|
2324
|
+
lineage: {
|
|
2325
|
+
childSessionId: input.childSession.id,
|
|
2326
|
+
parentSessionId,
|
|
2327
|
+
...input.childSession.parentTurnId ? { parentTurnId: input.childSession.parentTurnId } : {},
|
|
2328
|
+
...input.lineage ?? {}
|
|
2329
|
+
},
|
|
2330
|
+
personalConnectionDelegations
|
|
2331
|
+
},
|
|
2332
|
+
"summary",
|
|
2333
|
+
"summaryCodecVersion"
|
|
2334
|
+
),
|
|
2335
|
+
"payload",
|
|
2336
|
+
"payloadCodecVersion"
|
|
2337
|
+
)
|
|
2338
|
+
).onConflictDoNothing({
|
|
2339
|
+
target: [
|
|
2340
|
+
sessionSystemUpdateOutbox.workspaceId,
|
|
2341
|
+
sessionSystemUpdateOutbox.dedupeKey
|
|
2342
|
+
]
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
async function enqueueCancelledChildOutboxInTransaction(db, input) {
|
|
2346
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2347
|
+
workspaceId: input.workspaceId,
|
|
2348
|
+
childSession: input.rootSession,
|
|
2349
|
+
parentSession: input.parentSession,
|
|
2350
|
+
dedupeKey: `child-completion:${input.rootSession.id}:cancelled`,
|
|
2351
|
+
kind: "child_terminal_result",
|
|
2352
|
+
classification: "info",
|
|
2353
|
+
summary: "Child session was terminally cancelled.",
|
|
2354
|
+
payload: {
|
|
2355
|
+
type: "child_terminal_result",
|
|
2356
|
+
childSessionId: input.rootSession.id,
|
|
2357
|
+
status: "cancelled"
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
async function enqueueChildPausedOutboxInTransaction(db, input) {
|
|
2362
|
+
if (!childLifecycleNoticesEnabled()) return;
|
|
2363
|
+
const payload = {
|
|
2364
|
+
type: "child_paused",
|
|
2365
|
+
childSessionId: input.childSession.id,
|
|
2366
|
+
operationId: input.operationId,
|
|
2367
|
+
actorKind: input.actorKind,
|
|
2368
|
+
reason: boundedChildPausedReason(input.reason)
|
|
2369
|
+
};
|
|
2370
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2371
|
+
workspaceId: input.workspaceId,
|
|
2372
|
+
childSession: input.childSession,
|
|
2373
|
+
parentSession: input.parentSession,
|
|
2374
|
+
dedupeKey: childPausedDedupeKey({
|
|
2375
|
+
childSessionId: input.childSession.id,
|
|
2376
|
+
receiptId: input.operationId
|
|
2377
|
+
}),
|
|
2378
|
+
kind: "child_paused",
|
|
2379
|
+
classification: childPausedClassification(input.actorKind),
|
|
2380
|
+
summary: childPausedSummary(input.childSession.id, payload),
|
|
2381
|
+
payload
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
async function enqueueCancelledChildRequiresActionResolvedOutboxInTransaction(db, input) {
|
|
2385
|
+
if (!childLifecycleNoticesEnabled() || !input.parentSession) return;
|
|
2386
|
+
for (const humanInput of input.cancelledHumanInputs) {
|
|
2387
|
+
if (humanInput.sessionId !== input.rootSession.id) continue;
|
|
2388
|
+
const payload = {
|
|
2389
|
+
type: "child_requires_action_resolved",
|
|
2390
|
+
childSessionId: input.rootSession.id,
|
|
2391
|
+
childTurnId: humanInput.turnId,
|
|
2392
|
+
childTurnGeneration: humanInput.turnGeneration,
|
|
2393
|
+
requestId: humanInput.id,
|
|
2394
|
+
approvalId: null,
|
|
2395
|
+
outcome: "cancelled",
|
|
2396
|
+
respondedByKind: "system"
|
|
2397
|
+
};
|
|
2398
|
+
await insertChildOutboxRowInTransaction(db, {
|
|
2399
|
+
workspaceId: input.workspaceId,
|
|
2400
|
+
childSession: input.rootSession,
|
|
2401
|
+
parentSession: input.parentSession,
|
|
2402
|
+
dedupeKey: childRequiresActionResolvedDedupeKey({
|
|
2403
|
+
childSessionId: input.rootSession.id,
|
|
2404
|
+
turnId: humanInput.turnId,
|
|
2405
|
+
turnGeneration: humanInput.turnGeneration,
|
|
2406
|
+
requestId: humanInput.id,
|
|
2407
|
+
approvalId: null
|
|
2408
|
+
}),
|
|
2409
|
+
kind: "child_requires_action_resolved",
|
|
2410
|
+
classification: "info",
|
|
2411
|
+
summary: childRequiresActionResolvedSummary(input.rootSession.id, payload),
|
|
2412
|
+
payload,
|
|
2413
|
+
lineage: { turnId: humanInput.turnId }
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async function assertSessionBranchIsNotCancelled(db, workspaceId, sessionId) {
|
|
2418
|
+
const rows = await db.execute(sql3`
|
|
2419
|
+
with recursive ancestry(id, parent_id, status, depth, path, cycle) as (
|
|
2420
|
+
select session.id, session.parent_session_id, session.status, 0::integer,
|
|
2421
|
+
array[session.id]::uuid[], false
|
|
2422
|
+
from ${sessions} session
|
|
2423
|
+
where session.workspace_id = ${workspaceId} and session.id = ${sessionId}
|
|
2424
|
+
union all
|
|
2425
|
+
select parent.id, parent.parent_session_id, parent.status, child.depth + 1,
|
|
2426
|
+
child.path || parent.id, parent.id = any(child.path)
|
|
2427
|
+
from ancestry child
|
|
2428
|
+
join ${sessions} parent
|
|
2429
|
+
on parent.workspace_id = ${workspaceId} and parent.id = child.parent_id
|
|
2430
|
+
where child.depth < ${SESSION_ANCESTRY_LIMIT} and not child.cycle
|
|
2431
|
+
)
|
|
2432
|
+
select
|
|
2433
|
+
coalesce(bool_or(status = 'cancelled'), false) as cancelled,
|
|
2434
|
+
count(*) = 0 or coalesce(bool_or(cycle), false)
|
|
2435
|
+
or coalesce(max(depth), 0) >= ${SESSION_ANCESTRY_LIMIT} as invalid
|
|
2436
|
+
from ancestry
|
|
2437
|
+
`);
|
|
2438
|
+
if (rows[0]?.invalid) {
|
|
2439
|
+
throw new SessionControlInvariantError(`Session ${sessionId} ancestry is invalid`);
|
|
2440
|
+
}
|
|
2441
|
+
if (rows[0]?.cancelled) {
|
|
2442
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot accept work");
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function cancelSessionSubtreeInTransaction(db, input) {
|
|
2446
|
+
const candidateTurnIdSet = new Set(input.candidateTurnIds);
|
|
2447
|
+
const candidateTurns = input.lockedTurns.filter((turn) => candidateTurnIdSet.has(turn.id));
|
|
2448
|
+
if (candidateTurns.length !== candidateTurnIdSet.size) {
|
|
2449
|
+
throw new SessionControlInvariantError("Cancellation candidate turns changed while locking");
|
|
2450
|
+
}
|
|
2451
|
+
const suffixLocks = await lockSessionEventWriteRows(db, {
|
|
2452
|
+
workspaceId: input.workspaceId,
|
|
2453
|
+
controlLock: "already_locked",
|
|
2454
|
+
workspaceLock: "already_locked",
|
|
2455
|
+
sessionIds: input.lockedSessions.map((session) => session.id),
|
|
2456
|
+
turnIds: input.candidateTurnIds
|
|
2457
|
+
});
|
|
2458
|
+
if (suffixLocks.sessions.length !== input.lockedSessions.length || suffixLocks.turns.length !== candidateTurnIdSet.size) {
|
|
2459
|
+
throw new SessionControlInvariantError("Cancellation rows changed under canonical locks");
|
|
2460
|
+
}
|
|
2461
|
+
const sessionIdSet = new Set(input.sessionIds);
|
|
2462
|
+
const rootSession = input.lockedSessions.find((session) => session.id === input.rootSessionId);
|
|
2463
|
+
if (!rootSession) {
|
|
2464
|
+
throw new SessionControlInvariantError(
|
|
2465
|
+
`Cancellation root ${input.rootSessionId} was not locked with its subtree`
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
const rootParentSession = rootSession.parentSessionId ? input.lockedSessions.find((session) => session.id === rootSession.parentSessionId) ?? null : null;
|
|
2469
|
+
const candidateTurnById = new Map(candidateTurns.map((turn) => [turn.id, turn]));
|
|
2470
|
+
const liveTurnIds = new Set(
|
|
2471
|
+
input.lockedSessions.filter((session) => session.activeTurnId !== null).flatMap((session) => {
|
|
2472
|
+
const turn = session.activeTurnId ? candidateTurnById.get(session.activeTurnId) : void 0;
|
|
2473
|
+
if (turn?.activeAttemptId === null) return [];
|
|
2474
|
+
return turn ? [turn.id] : [];
|
|
2475
|
+
})
|
|
2476
|
+
);
|
|
2477
|
+
const immediatelyCancelledTurns = candidateTurns.filter((turn) => !liveTurnIds.has(turn.id));
|
|
2478
|
+
const cancelledTurnsBySession = /* @__PURE__ */ new Map();
|
|
2479
|
+
for (const turn of immediatelyCancelledTurns) {
|
|
2480
|
+
const turns = cancelledTurnsBySession.get(turn.sessionId);
|
|
2481
|
+
if (turns) turns.push(turn);
|
|
2482
|
+
else cancelledTurnsBySession.set(turn.sessionId, [turn]);
|
|
2483
|
+
}
|
|
2484
|
+
const immediatelyCancelledTurnIds = immediatelyCancelledTurns.map((turn) => turn.id);
|
|
2485
|
+
const now = /* @__PURE__ */ new Date();
|
|
2486
|
+
let cancelledHumanInputs = [];
|
|
2487
|
+
if (immediatelyCancelledTurnIds.length > 0) {
|
|
2488
|
+
await db.update(sessionTurns).set({
|
|
2489
|
+
status: "cancelled",
|
|
2490
|
+
activeAttemptId: null,
|
|
2491
|
+
cancelledBy: input.actor,
|
|
2492
|
+
cancelReason: input.reason ?? "session_cancelled",
|
|
2493
|
+
version: sql3`${sessionTurns.version} + 1`,
|
|
2494
|
+
finishedAt: now,
|
|
2495
|
+
updatedAt: now
|
|
2496
|
+
}).where(inArray2(sessionTurns.id, immediatelyCancelledTurnIds));
|
|
2497
|
+
cancelledHumanInputs = await db.update(sessionHumanInputRequests).set({
|
|
2498
|
+
status: "cancelled",
|
|
2499
|
+
response: { outcome: "cancelled" },
|
|
2500
|
+
respondedBy: input.actor,
|
|
2501
|
+
respondedAt: now,
|
|
2502
|
+
updatedAt: now
|
|
2503
|
+
}).where(
|
|
2504
|
+
and3(
|
|
2505
|
+
eq3(sessionHumanInputRequests.workspaceId, input.workspaceId),
|
|
2506
|
+
inArray2(sessionHumanInputRequests.turnId, immediatelyCancelledTurnIds),
|
|
2507
|
+
eq3(sessionHumanInputRequests.status, "pending")
|
|
2508
|
+
)
|
|
2509
|
+
).returning({
|
|
2510
|
+
id: sessionHumanInputRequests.id,
|
|
2511
|
+
sessionId: sessionHumanInputRequests.sessionId,
|
|
2512
|
+
turnId: sessionHumanInputRequests.turnId,
|
|
2513
|
+
turnGeneration: sessionHumanInputRequests.turnGeneration,
|
|
2514
|
+
questions: sessionHumanInputRequests.questions
|
|
2515
|
+
});
|
|
2516
|
+
await db.update(codexCapacityWaiters).set({
|
|
2517
|
+
status: "superseded",
|
|
2518
|
+
lastWakeReason: "session_cancelled",
|
|
2519
|
+
updatedAt: now
|
|
2520
|
+
}).where(
|
|
2521
|
+
and3(
|
|
2522
|
+
eq3(codexCapacityWaiters.workspaceId, input.workspaceId),
|
|
2523
|
+
inArray2(codexCapacityWaiters.blockedTurnId, immediatelyCancelledTurnIds),
|
|
2524
|
+
eq3(codexCapacityWaiters.status, "waiting")
|
|
2525
|
+
)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
const cancelledSystemUpdates = await db.update(sessionSystemUpdates).set({ state: "cancelled" }).where(
|
|
2529
|
+
and3(
|
|
2530
|
+
eq3(sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
2531
|
+
inArray2(sessionSystemUpdates.sessionId, input.sessionIds),
|
|
2532
|
+
eq3(sessionSystemUpdates.state, "pending")
|
|
2533
|
+
)
|
|
2534
|
+
).returning({
|
|
2535
|
+
id: sessionSystemUpdates.id,
|
|
2536
|
+
sessionId: sessionSystemUpdates.sessionId
|
|
2537
|
+
});
|
|
2538
|
+
const humanInputsByTurn = /* @__PURE__ */ new Map();
|
|
2539
|
+
for (const humanInput of cancelledHumanInputs) {
|
|
2540
|
+
const inputs = humanInputsByTurn.get(humanInput.turnId);
|
|
2541
|
+
if (inputs) inputs.push(humanInput);
|
|
2542
|
+
else humanInputsByTurn.set(humanInput.turnId, [humanInput]);
|
|
2543
|
+
}
|
|
2544
|
+
const systemUpdatesBySession = /* @__PURE__ */ new Map();
|
|
2545
|
+
for (const update of cancelledSystemUpdates) {
|
|
2546
|
+
const updates = systemUpdatesBySession.get(update.sessionId);
|
|
2547
|
+
if (updates) updates.push(update);
|
|
2548
|
+
else systemUpdatesBySession.set(update.sessionId, [update]);
|
|
2549
|
+
}
|
|
2550
|
+
const affectedSessionEvents = [];
|
|
2551
|
+
for (const session of input.lockedSessions) {
|
|
2552
|
+
if (!sessionIdSet.has(session.id)) continue;
|
|
2553
|
+
let sequence = session.lastSequence + (session.id === input.rootSessionId ? 1 : 0);
|
|
2554
|
+
const cancelledTurns = cancelledTurnsBySession.get(session.id) ?? [];
|
|
2555
|
+
const preinsertedEvents = [];
|
|
2556
|
+
const eventValues = [];
|
|
2557
|
+
for (const update of (systemUpdatesBySession.get(session.id) ?? []).sort(
|
|
2558
|
+
(left, right) => left.id.localeCompare(right.id)
|
|
2559
|
+
)) {
|
|
2560
|
+
eventValues.push({
|
|
2561
|
+
accountId: input.accountId,
|
|
2562
|
+
workspaceId: input.workspaceId,
|
|
2563
|
+
sessionId: session.id,
|
|
2564
|
+
sequence: ++sequence,
|
|
2565
|
+
type: "system.update.cancelled",
|
|
2566
|
+
payload: { updateId: update.id, reason: "session_cancelled" },
|
|
2567
|
+
occurredAt: now
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
for (const turn of cancelledTurns) {
|
|
2571
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(db, {
|
|
2572
|
+
accountId: input.accountId,
|
|
2573
|
+
workspaceId: input.workspaceId,
|
|
2574
|
+
sessionId: session.id,
|
|
2575
|
+
turnId: turn.id,
|
|
2576
|
+
reason: "session_cancelled",
|
|
2577
|
+
sequence,
|
|
2578
|
+
now
|
|
2579
|
+
});
|
|
2580
|
+
sequence = closedTools.sequence;
|
|
2581
|
+
preinsertedEvents.push(
|
|
2582
|
+
...closedTools.events.map((event) => ({
|
|
2583
|
+
id: event.id,
|
|
2584
|
+
sequence: event.sequence
|
|
2585
|
+
}))
|
|
2586
|
+
);
|
|
2587
|
+
for (const humanInput of (humanInputsByTurn.get(turn.id) ?? []).sort(
|
|
2588
|
+
(left, right) => left.id.localeCompare(right.id)
|
|
2589
|
+
)) {
|
|
2590
|
+
eventValues.push({
|
|
2591
|
+
accountId: input.accountId,
|
|
2592
|
+
workspaceId: input.workspaceId,
|
|
2593
|
+
sessionId: session.id,
|
|
2594
|
+
sequence: ++sequence,
|
|
2595
|
+
type: "user.humanInputResponse",
|
|
2596
|
+
turnId: turn.id,
|
|
2597
|
+
turnGeneration: turn.executionGeneration,
|
|
2598
|
+
...turn.id === session.activeTurnId ? { turnAssociation: "current" } : {},
|
|
2599
|
+
payload: {
|
|
2600
|
+
requestId: humanInput.id,
|
|
2601
|
+
response: { outcome: "cancelled" }
|
|
2602
|
+
},
|
|
2603
|
+
occurredAt: now
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
eventValues.push({
|
|
2607
|
+
accountId: input.accountId,
|
|
2608
|
+
workspaceId: input.workspaceId,
|
|
2609
|
+
sessionId: session.id,
|
|
2610
|
+
sequence: ++sequence,
|
|
2611
|
+
type: "turn.cancelled",
|
|
2612
|
+
turnId: turn.id,
|
|
2613
|
+
turnGeneration: turn.executionGeneration,
|
|
2614
|
+
...turn.id === session.activeTurnId ? { turnAssociation: "current" } : {},
|
|
2615
|
+
payload: {
|
|
2616
|
+
reason: input.reason ?? "session_cancelled",
|
|
2617
|
+
operationId: input.operationId
|
|
2618
|
+
},
|
|
2619
|
+
occurredAt: now
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
if (session.status !== "cancelled") {
|
|
2623
|
+
eventValues.push({
|
|
2624
|
+
accountId: input.accountId,
|
|
2625
|
+
workspaceId: input.workspaceId,
|
|
2626
|
+
sessionId: session.id,
|
|
2627
|
+
sequence: ++sequence,
|
|
2628
|
+
type: "session.status.changed",
|
|
2629
|
+
payload: {
|
|
2630
|
+
status: "cancelled",
|
|
2631
|
+
reason: input.reason ?? "session_cancelled",
|
|
2632
|
+
operationId: input.operationId
|
|
2633
|
+
},
|
|
2634
|
+
occurredAt: now
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
const inserted = eventValues.length > 0 ? await db.insert(sessionEvents).values(withLosslessContentWriteVersion(eventValues, "payload", "payloadCodecVersion")).returning({
|
|
2638
|
+
id: sessionEvents.id,
|
|
2639
|
+
sequence: sessionEvents.sequence,
|
|
2640
|
+
type: sessionEvents.type,
|
|
2641
|
+
turnId: sessionEvents.turnId,
|
|
2642
|
+
payload: sessionEvents.payload
|
|
2643
|
+
}) : [];
|
|
2644
|
+
const cancelledHumanInputsById = new Map(
|
|
2645
|
+
cancelledHumanInputs.filter((request) => request.sessionId === session.id).map((request) => [request.id, request])
|
|
2646
|
+
);
|
|
2647
|
+
for (const event of inserted) {
|
|
2648
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
2649
|
+
const payload = event.payload;
|
|
2650
|
+
const request = typeof payload.requestId === "string" ? cancelledHumanInputsById.get(payload.requestId) : null;
|
|
2651
|
+
if (!request) continue;
|
|
2652
|
+
await mirrorSessionRealtimeContextInTransaction(db, {
|
|
2653
|
+
accountId: input.accountId,
|
|
2654
|
+
workspaceId: input.workspaceId,
|
|
2655
|
+
sessionId: session.id,
|
|
2656
|
+
sourceKind: "human_input_response",
|
|
2657
|
+
sourceId: event.id,
|
|
2658
|
+
turnId: event.turnId,
|
|
2659
|
+
channel: null,
|
|
2660
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
2661
|
+
requestId: request.id,
|
|
2662
|
+
questions: request.questions,
|
|
2663
|
+
response: { outcome: "cancelled" }
|
|
2664
|
+
}),
|
|
2665
|
+
payload: {
|
|
2666
|
+
requestId: request.id,
|
|
2667
|
+
outcome: "cancelled",
|
|
2668
|
+
sourceEventId: event.id
|
|
2669
|
+
},
|
|
2670
|
+
now
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
const liveTurnId = session.activeTurnId && liveTurnIds.has(session.activeTurnId) ? session.activeTurnId : null;
|
|
2674
|
+
await db.update(sessions).set({
|
|
2675
|
+
status: "cancelled",
|
|
2676
|
+
activeTurnId: liveTurnId,
|
|
2677
|
+
queueVersion: sql3`${sessions.queueVersion} + 1`,
|
|
2678
|
+
queueHeadPosition: 0,
|
|
2679
|
+
queueTailPosition: 0,
|
|
2680
|
+
lastSequence: sequence,
|
|
2681
|
+
updatedAt: now
|
|
2682
|
+
}).where(
|
|
2683
|
+
and3(eq3(sessions.workspaceId, input.workspaceId), eq3(sessions.id, session.id))
|
|
2684
|
+
);
|
|
2685
|
+
const eventIds = [...preinsertedEvents, ...inserted].sort((left, right) => left.sequence - right.sequence).map((event) => event.id);
|
|
2686
|
+
if (session.id === input.rootSessionId) eventIds.unshift(input.rootControlEventId);
|
|
2687
|
+
if (eventIds.length > 0) affectedSessionEvents.push({ sessionId: session.id, eventIds });
|
|
2688
|
+
}
|
|
2689
|
+
await enqueueCancelledChildRequiresActionResolvedOutboxInTransaction(db, {
|
|
2690
|
+
workspaceId: input.workspaceId,
|
|
2691
|
+
rootSession,
|
|
2692
|
+
parentSession: rootParentSession,
|
|
2693
|
+
cancelledHumanInputs
|
|
2694
|
+
});
|
|
2695
|
+
await enqueueCancelledChildOutboxInTransaction(db, {
|
|
2696
|
+
workspaceId: input.workspaceId,
|
|
2697
|
+
rootSession,
|
|
2698
|
+
parentSession: rootParentSession
|
|
2699
|
+
});
|
|
2700
|
+
return {
|
|
2701
|
+
cancelledSessionCount: input.sessionIds.length,
|
|
2702
|
+
cancelledTurnCount: candidateTurns.length,
|
|
2703
|
+
affectedSessionEvents
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
async function mutateSessionControlInTransaction(db, input) {
|
|
2707
|
+
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
2708
|
+
...input.controlLockTimeoutMs !== void 0 ? { lockTimeoutMs: input.controlLockTimeoutMs } : {}
|
|
2709
|
+
});
|
|
2710
|
+
const cancellationSubtree = input.action === "cancel" ? await loadSessionSubtreeIds(db, input.workspaceId, input.sessionId) : { sessionIds: [input.sessionId], rootParentSessionId: null };
|
|
2711
|
+
const cancellationSessionIds = cancellationSubtree.sessionIds;
|
|
2712
|
+
const cancellationTurnIds = input.action === "cancel" ? await loadCancellationTurnIds(db, input.workspaceId, cancellationSessionIds) : [];
|
|
2713
|
+
const pauseParentSessionId = input.action === "pause" && childLifecycleNoticesEnabled() ? await loadParentSessionId(db, input.workspaceId, input.sessionId) : null;
|
|
2714
|
+
const lineageParentSessionId = cancellationSubtree.rootParentSessionId ?? pauseParentSessionId;
|
|
2715
|
+
const locks = await lockSessionEventWriteRows(db, {
|
|
2716
|
+
workspaceId: input.workspaceId,
|
|
2717
|
+
controlLock: "already_locked",
|
|
2718
|
+
sessionIds: input.actor.type === "agent_attempt" ? [
|
|
2719
|
+
input.actor.sessionId,
|
|
2720
|
+
...cancellationSessionIds,
|
|
2721
|
+
...lineageParentSessionId ? [lineageParentSessionId] : []
|
|
2722
|
+
] : [...cancellationSessionIds, ...lineageParentSessionId ? [lineageParentSessionId] : []],
|
|
2723
|
+
turnIds: input.actor.type === "agent_attempt" ? [input.actor.turnId, ...cancellationTurnIds] : cancellationTurnIds,
|
|
2724
|
+
attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : []
|
|
2725
|
+
});
|
|
2726
|
+
if (input.action === "cancel" || pauseParentSessionId !== null) {
|
|
2727
|
+
const rootSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2728
|
+
if (!rootSession || rootSession.parentSessionId !== lineageParentSessionId) {
|
|
2729
|
+
throw new SessionControlInvariantError(
|
|
2730
|
+
`Session ${input.sessionId} parent changed while establishing ${input.action} locks`
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const hash = canonicalSessionCommandHash({
|
|
2735
|
+
action: input.action,
|
|
2736
|
+
reason: input.reason ?? null,
|
|
2737
|
+
expectedControlEtag: input.expectedControlEtag ?? null
|
|
2738
|
+
});
|
|
2739
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
2740
|
+
accountId: input.accountId,
|
|
2741
|
+
workspaceId: input.workspaceId,
|
|
2742
|
+
actor: input.actor,
|
|
2743
|
+
action: `session.${input.action}`,
|
|
2744
|
+
targetSessionId: input.sessionId,
|
|
2745
|
+
targetTurnId: null,
|
|
2746
|
+
operationKey: input.operationKey,
|
|
2747
|
+
canonicalRequestHash: hash
|
|
2748
|
+
});
|
|
2749
|
+
if (reserved.replay) {
|
|
2750
|
+
const storedOutcome = reserved.receipt.result.outcome;
|
|
2751
|
+
const unchanged = storedOutcome === "unchanged" && reserved.receipt.appliedControlRevision === null;
|
|
2752
|
+
if (!unchanged && reserved.receipt.appliedControlRevision === null) {
|
|
2753
|
+
throw new SessionControlInvariantError(
|
|
2754
|
+
"Replayed session control receipt has no applied control revision"
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
const workspaceControlEventId2 = unchanged ? null : String(reserved.receipt.result.workspaceControlEventId ?? "");
|
|
2758
|
+
if (!unchanged && !workspaceControlEventId2) {
|
|
2759
|
+
throw new SessionControlInvariantError("Replayed session control receipt has no event");
|
|
2760
|
+
}
|
|
2761
|
+
const sessionControlEventId = unchanged ? null : String(reserved.receipt.result.eventId ?? "");
|
|
2762
|
+
if (!unchanged && !sessionControlEventId) {
|
|
2763
|
+
throw new SessionControlInvariantError(
|
|
2764
|
+
"Replayed session control receipt has no session event"
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
const wakeCount2 = Number(reserved.receipt.result.wakeCount ?? 0);
|
|
2768
|
+
return {
|
|
2769
|
+
receipt: reserved.receipt,
|
|
2770
|
+
control: await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
2771
|
+
workspaceControl: workspace
|
|
2772
|
+
}),
|
|
2773
|
+
sessionControlEventId,
|
|
2774
|
+
workspaceControlEventId: workspaceControlEventId2,
|
|
2775
|
+
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
2776
|
+
backgroundCommandCount: Number(reserved.receipt.result.backgroundCommandCount ?? 0),
|
|
2777
|
+
wakeCount: wakeCount2,
|
|
2778
|
+
cancelledSessionCount: Number(reserved.receipt.result.cancelledSessionCount ?? 0),
|
|
2779
|
+
cancelledTurnCount: Number(reserved.receipt.result.cancelledTurnCount ?? 0),
|
|
2780
|
+
affectedSessionEvents: Array.isArray(reserved.receipt.result.affectedSessionEvents) ? reserved.receipt.result.affectedSessionEvents : sessionControlEventId ? [{ sessionId: input.sessionId, eventIds: [sessionControlEventId] }] : [],
|
|
2781
|
+
workflowWake: await pendingSessionControlWorkflowWake(
|
|
2782
|
+
db,
|
|
2783
|
+
input.workspaceId,
|
|
2784
|
+
input.sessionId,
|
|
2785
|
+
wakeCount2
|
|
2786
|
+
),
|
|
2787
|
+
outcome: "replayed",
|
|
2788
|
+
replay: true
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
if (input.actor.type === "agent_attempt") {
|
|
2792
|
+
await assertAgentCommandAuthorityInTransaction(db, {
|
|
2793
|
+
workspaceId: input.workspaceId,
|
|
2794
|
+
actor: input.actor,
|
|
2795
|
+
targetSessionId: input.sessionId,
|
|
2796
|
+
action: input.action === "cancel" ? "pause" : input.action
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
if (input.action === "resume") {
|
|
2800
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2801
|
+
}
|
|
2802
|
+
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
2803
|
+
workspaceControl: workspace
|
|
2804
|
+
});
|
|
2805
|
+
if (input.expectedControlEtag && input.expectedControlEtag !== before.controlEtag) {
|
|
2806
|
+
throw new SessionControlConflictError();
|
|
2807
|
+
}
|
|
2808
|
+
if (input.action !== "cancel") {
|
|
2809
|
+
const targetSession2 = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2810
|
+
if (!targetSession2) {
|
|
2811
|
+
throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
|
|
2812
|
+
}
|
|
2813
|
+
const directPauseRevision = asSafeRevision(
|
|
2814
|
+
targetSession2.directPauseRevision,
|
|
2815
|
+
"direct pause revision"
|
|
2816
|
+
);
|
|
2817
|
+
const changed = input.action === "pause" ? targetSession2.directControlState !== "paused" || directPauseRevision === null || await sessionPauseEffectsNeeded(db, {
|
|
2818
|
+
workspaceId: input.workspaceId,
|
|
2819
|
+
sessionId: input.sessionId,
|
|
2820
|
+
directPauseRevision
|
|
2821
|
+
}) : before.state === "paused" || await continuableWakeRepairNeeded(db, {
|
|
2822
|
+
workspaceId: input.workspaceId,
|
|
2823
|
+
rootSessionId: input.sessionId
|
|
2824
|
+
});
|
|
2825
|
+
if (!changed) {
|
|
2826
|
+
const [quiescence] = input.action === "pause" ? await db.execute(sql3`
|
|
2827
|
+
select exists (
|
|
2828
|
+
select 1 from ${sessionTurnAttempts} attempt
|
|
2829
|
+
where attempt.workspace_id = ${input.workspaceId}
|
|
2830
|
+
and attempt.session_id = ${input.sessionId}
|
|
2831
|
+
and attempt.state = 'closed'
|
|
2832
|
+
and attempt.quiesced_at is null
|
|
2833
|
+
and exists (
|
|
2834
|
+
select 1 from ${sessionAttemptInterruptions} interruption
|
|
2835
|
+
where interruption.workspace_id = attempt.workspace_id
|
|
2836
|
+
and interruption.session_id = attempt.session_id
|
|
2837
|
+
and interruption.attempt_id = attempt.id
|
|
2838
|
+
and interruption.state in ('settled', 'rejected_stale')
|
|
2839
|
+
)
|
|
2840
|
+
) as needed
|
|
2841
|
+
`) : [];
|
|
2842
|
+
const wakeCount2 = quiescence?.needed ? 1 : 0;
|
|
2843
|
+
if (wakeCount2 > 0) {
|
|
2844
|
+
await registerSessionWorkflowWakeInTransaction(db, {
|
|
2845
|
+
accountId: input.accountId,
|
|
2846
|
+
workspaceId: input.workspaceId,
|
|
2847
|
+
sessionId: input.sessionId,
|
|
2848
|
+
temporalWorkflowId: targetSession2.temporalWorkflowId ?? `session-${input.sessionId}`,
|
|
2849
|
+
reason: "session_pause_quiescence_reconciliation"
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
const receipt2 = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
2853
|
+
result: {
|
|
2854
|
+
outcome: "unchanged",
|
|
2855
|
+
interruptionCount: 0,
|
|
2856
|
+
backgroundCommandCount: 0,
|
|
2857
|
+
wakeCount: wakeCount2,
|
|
2858
|
+
cancelledSessionCount: 0,
|
|
2859
|
+
cancelledTurnCount: 0,
|
|
2860
|
+
affectedSessionEvents: []
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
return {
|
|
2864
|
+
receipt: receipt2,
|
|
2865
|
+
control: before,
|
|
2866
|
+
sessionControlEventId: null,
|
|
2867
|
+
workspaceControlEventId: null,
|
|
2868
|
+
interruptionCount: 0,
|
|
2869
|
+
backgroundCommandCount: 0,
|
|
2870
|
+
wakeCount: wakeCount2,
|
|
2871
|
+
cancelledSessionCount: 0,
|
|
2872
|
+
cancelledTurnCount: 0,
|
|
2873
|
+
affectedSessionEvents: [],
|
|
2874
|
+
workflowWake: await pendingSessionControlWorkflowWake(
|
|
2875
|
+
db,
|
|
2876
|
+
input.workspaceId,
|
|
2877
|
+
input.sessionId,
|
|
2878
|
+
wakeCount2
|
|
2879
|
+
),
|
|
2880
|
+
outcome: "unchanged",
|
|
2881
|
+
replay: false
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
const targetSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2886
|
+
if (!targetSession) {
|
|
2887
|
+
throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
|
|
2888
|
+
}
|
|
2889
|
+
const revision = nextRevision(workspace);
|
|
2890
|
+
await advanceWorkspaceRevision(db, input.workspaceId, revision);
|
|
2891
|
+
const updatedRows = await db.update(sessions).set(
|
|
2892
|
+
input.action === "pause" || input.action === "cancel" ? {
|
|
2893
|
+
directControlState: "paused",
|
|
2894
|
+
directPauseRevision: revision,
|
|
2895
|
+
controlVersion: revision,
|
|
2896
|
+
directControlReason: input.reason ?? null,
|
|
2897
|
+
directControlChangedBy: input.actor.type === "agent_attempt" ? `attempt:${input.actor.attemptId}` : input.actor.subjectId,
|
|
2898
|
+
directControlChangedAt: /* @__PURE__ */ new Date(),
|
|
2899
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2900
|
+
} : {
|
|
2901
|
+
directControlState: "active",
|
|
2902
|
+
directPauseRevision: null,
|
|
2903
|
+
subtreeRunOverrideRevision: revision,
|
|
2904
|
+
controlVersion: revision,
|
|
2905
|
+
directControlReason: input.reason ?? null,
|
|
2906
|
+
directControlChangedBy: input.actor.type === "agent_attempt" ? `attempt:${input.actor.attemptId}` : input.actor.subjectId,
|
|
2907
|
+
directControlChangedAt: /* @__PURE__ */ new Date(),
|
|
2908
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2909
|
+
}
|
|
2910
|
+
).where(
|
|
2911
|
+
and3(
|
|
2912
|
+
eq3(sessions.workspaceId, input.workspaceId),
|
|
2913
|
+
input.action === "cancel" ? inArray2(sessions.id, cancellationSessionIds) : eq3(sessions.id, input.sessionId)
|
|
2914
|
+
)
|
|
2915
|
+
).returning({ id: sessions.id });
|
|
2916
|
+
const updated = updatedRows.find((row) => row.id === input.sessionId);
|
|
2917
|
+
if (!updated) throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
|
|
2918
|
+
const actor = input.actor.type === "agent_attempt" ? `attempt:${input.actor.attemptId}` : input.actor.subjectId;
|
|
2919
|
+
const backgroundCommandCount = input.action === "pause" || input.action === "cancel" ? await requestSessionBackgroundCommandCancellationsInTransaction(db, {
|
|
2920
|
+
workspaceId: input.workspaceId,
|
|
2921
|
+
rootSessionIds: [input.sessionId],
|
|
2922
|
+
subjectId: actor
|
|
2923
|
+
}) : 0;
|
|
2924
|
+
const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
|
|
2925
|
+
accountId: input.accountId,
|
|
2926
|
+
workspaceId: input.workspaceId,
|
|
2927
|
+
revision,
|
|
2928
|
+
scope: "session",
|
|
2929
|
+
rootSessionId: input.sessionId,
|
|
2930
|
+
action: input.action === "cancel" ? "pause" : input.action,
|
|
2931
|
+
automatic: false,
|
|
2932
|
+
reason: input.reason ?? null,
|
|
2933
|
+
actor
|
|
2934
|
+
});
|
|
2935
|
+
const interruptionCount = input.action === "pause" || input.action === "cancel" ? await interruptDescendantAttempts(db, {
|
|
2936
|
+
accountId: input.accountId,
|
|
2937
|
+
workspaceId: input.workspaceId,
|
|
2938
|
+
sessionId: input.sessionId,
|
|
2939
|
+
operationId: reserved.receipt.id,
|
|
2940
|
+
kind: "session_pause",
|
|
2941
|
+
controlRevision: revision
|
|
2942
|
+
}) : 0;
|
|
2943
|
+
const wakeCount = input.action === "cancel" ? await registerCancellationWakes(db, {
|
|
2944
|
+
workspaceId: input.workspaceId,
|
|
2945
|
+
sessionIds: cancellationSessionIds
|
|
2946
|
+
}) : input.action === "pause" ? await registerInterruptionWakes(db, {
|
|
2947
|
+
operationId: reserved.receipt.id,
|
|
2948
|
+
reason: "session_pause_interruption"
|
|
2949
|
+
}) : await registerDescendantWakes(db, {
|
|
2950
|
+
workspaceId: input.workspaceId,
|
|
2951
|
+
sessionId: input.sessionId,
|
|
2952
|
+
reason: "session_resume"
|
|
2953
|
+
});
|
|
2954
|
+
const [controlEvent] = await db.insert(sessionEvents).values(
|
|
2955
|
+
withLosslessContentWriteVersion(
|
|
2956
|
+
{
|
|
2957
|
+
accountId: input.accountId,
|
|
2958
|
+
workspaceId: input.workspaceId,
|
|
2959
|
+
sessionId: input.sessionId,
|
|
2960
|
+
sequence: targetSession.lastSequence + 1,
|
|
2961
|
+
type: input.action === "pause" || input.action === "cancel" ? "session.control.paused" : "session.control.resumed",
|
|
2962
|
+
payload: {
|
|
2963
|
+
operationId: reserved.receipt.id,
|
|
2964
|
+
revision,
|
|
2965
|
+
actor,
|
|
2966
|
+
...input.reason ? { reason: input.reason } : {},
|
|
2967
|
+
...input.action === "cancel" ? { terminal: true } : {},
|
|
2968
|
+
interruptionCount
|
|
2969
|
+
},
|
|
2970
|
+
occurredAt: /* @__PURE__ */ new Date()
|
|
2971
|
+
},
|
|
2972
|
+
"payload",
|
|
2973
|
+
"payloadCodecVersion"
|
|
2974
|
+
)
|
|
2975
|
+
).returning({ id: sessionEvents.id });
|
|
2976
|
+
if (!controlEvent)
|
|
2977
|
+
throw new SessionControlInvariantError("Session control event was not inserted");
|
|
2978
|
+
await db.update(sessions).set({ lastSequence: targetSession.lastSequence + 1, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(sessions.id, input.sessionId));
|
|
2979
|
+
await db.insert(auditEvents).values(
|
|
2980
|
+
withLosslessContentWriteVersion(
|
|
2981
|
+
{
|
|
2982
|
+
accountId: input.accountId,
|
|
2983
|
+
workspaceId: input.workspaceId,
|
|
2984
|
+
subjectId: actor,
|
|
2985
|
+
action: `session.control.${input.action}`,
|
|
2986
|
+
targetType: "session",
|
|
2987
|
+
targetId: input.sessionId,
|
|
2988
|
+
metadata: {
|
|
2989
|
+
operationId: reserved.receipt.id,
|
|
2990
|
+
revision,
|
|
2991
|
+
interruptionCount,
|
|
2992
|
+
...input.actor.type === "agent_attempt" ? {
|
|
2993
|
+
callerSessionId: input.actor.sessionId,
|
|
2994
|
+
callerTurnId: input.actor.turnId,
|
|
2995
|
+
callerAttemptId: input.actor.attemptId,
|
|
2996
|
+
callerExecutionGeneration: input.actor.executionGeneration
|
|
2997
|
+
} : {}
|
|
2998
|
+
}
|
|
2999
|
+
},
|
|
3000
|
+
"metadata",
|
|
3001
|
+
"metadataCodecVersion"
|
|
3002
|
+
)
|
|
3003
|
+
);
|
|
3004
|
+
const cancellation = input.action === "cancel" ? await cancelSessionSubtreeInTransaction(db, {
|
|
3005
|
+
accountId: input.accountId,
|
|
3006
|
+
workspaceId: input.workspaceId,
|
|
3007
|
+
rootSessionId: input.sessionId,
|
|
3008
|
+
sessionIds: cancellationSessionIds,
|
|
3009
|
+
lockedSessions: locks.sessions,
|
|
3010
|
+
candidateTurnIds: cancellationTurnIds,
|
|
3011
|
+
lockedTurns: locks.turns,
|
|
3012
|
+
actor,
|
|
3013
|
+
reason: input.reason ?? null,
|
|
3014
|
+
operationId: reserved.receipt.id,
|
|
3015
|
+
rootControlEventId: controlEvent.id
|
|
3016
|
+
}) : {
|
|
3017
|
+
cancelledSessionCount: 0,
|
|
3018
|
+
cancelledTurnCount: 0,
|
|
3019
|
+
affectedSessionEvents: [{ sessionId: input.sessionId, eventIds: [controlEvent.id] }]
|
|
3020
|
+
};
|
|
3021
|
+
if (input.action === "pause" && pauseParentSessionId !== null) {
|
|
3022
|
+
const pausedSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
3023
|
+
const parentSession = locks.sessions.find((session) => session.id === pauseParentSessionId);
|
|
3024
|
+
const issuedByParentAttempt = input.actor.type === "agent_attempt" && input.actor.sessionId === pauseParentSessionId;
|
|
3025
|
+
if (pausedSession && parentSession && !issuedByParentAttempt) {
|
|
3026
|
+
await enqueueChildPausedOutboxInTransaction(db, {
|
|
3027
|
+
workspaceId: input.workspaceId,
|
|
3028
|
+
childSession: pausedSession,
|
|
3029
|
+
parentSession,
|
|
3030
|
+
operationId: reserved.receipt.id,
|
|
3031
|
+
actorKind: input.actor.type === "agent_attempt" ? "agent" : input.actor.type === "service" ? "api" : "human",
|
|
3032
|
+
reason: input.reason ?? null
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
3037
|
+
controlRevision: revision,
|
|
3038
|
+
result: {
|
|
3039
|
+
outcome: "changed",
|
|
3040
|
+
interruptionCount,
|
|
3041
|
+
backgroundCommandCount,
|
|
3042
|
+
wakeCount,
|
|
3043
|
+
eventId: controlEvent.id,
|
|
3044
|
+
workspaceControlEventId,
|
|
3045
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
3046
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
3047
|
+
affectedSessionEvents: cancellation.affectedSessionEvents
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
const control = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
3051
|
+
lock: "share"
|
|
3052
|
+
});
|
|
3053
|
+
return {
|
|
3054
|
+
receipt,
|
|
3055
|
+
control,
|
|
3056
|
+
sessionControlEventId: controlEvent.id,
|
|
3057
|
+
workspaceControlEventId,
|
|
3058
|
+
interruptionCount,
|
|
3059
|
+
backgroundCommandCount,
|
|
3060
|
+
wakeCount,
|
|
3061
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
3062
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
3063
|
+
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
3064
|
+
workflowWake: await pendingSessionControlWorkflowWake(
|
|
3065
|
+
db,
|
|
3066
|
+
input.workspaceId,
|
|
3067
|
+
input.sessionId,
|
|
3068
|
+
wakeCount
|
|
3069
|
+
),
|
|
3070
|
+
outcome: "changed",
|
|
3071
|
+
replay: false
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
async function autoResumeSessionBranchInTransaction(db, input) {
|
|
3075
|
+
const workspace = input.admission?.control ?? await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
3076
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
3077
|
+
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
3078
|
+
workspaceControl: workspace
|
|
3079
|
+
});
|
|
3080
|
+
if (input.observedControlEtag && input.observedControlEtag !== before.controlEtag) {
|
|
3081
|
+
throw new SessionControlConflictError();
|
|
3082
|
+
}
|
|
3083
|
+
if (before.state === "active") {
|
|
3084
|
+
return {
|
|
3085
|
+
revision: asSafeRevision(workspace.revision, "workspace control revision"),
|
|
3086
|
+
control: before,
|
|
3087
|
+
changed: false,
|
|
3088
|
+
workspaceControlEventId: null
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
if (input.admission?.mode === "share") {
|
|
3092
|
+
throw new SessionControlInvariantError(
|
|
3093
|
+
`Session ${input.sessionId} became paused under a shared workspace control lock`
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
const revision = nextRevision(workspace);
|
|
3097
|
+
await advanceWorkspaceRevision(db, input.workspaceId, revision);
|
|
3098
|
+
await db.update(sessions).set({
|
|
3099
|
+
directControlState: "active",
|
|
3100
|
+
directPauseRevision: null,
|
|
3101
|
+
subtreeRunOverrideRevision: revision,
|
|
3102
|
+
controlVersion: revision,
|
|
3103
|
+
directControlReason: input.reason,
|
|
3104
|
+
directControlChangedBy: input.actor,
|
|
3105
|
+
directControlChangedAt: /* @__PURE__ */ new Date(),
|
|
3106
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3107
|
+
}).where(
|
|
3108
|
+
and3(
|
|
3109
|
+
eq3(sessions.workspaceId, input.workspaceId),
|
|
3110
|
+
eq3(sessions.id, input.sessionId)
|
|
3111
|
+
)
|
|
3112
|
+
);
|
|
3113
|
+
const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
|
|
3114
|
+
accountId: workspace.accountId,
|
|
3115
|
+
workspaceId: input.workspaceId,
|
|
3116
|
+
revision,
|
|
3117
|
+
scope: "session",
|
|
3118
|
+
rootSessionId: input.sessionId,
|
|
3119
|
+
action: "resume",
|
|
3120
|
+
automatic: true,
|
|
3121
|
+
reason: input.reason,
|
|
3122
|
+
actor: input.actor
|
|
3123
|
+
});
|
|
3124
|
+
return {
|
|
3125
|
+
revision,
|
|
3126
|
+
control: await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
3127
|
+
lock: "share"
|
|
3128
|
+
}),
|
|
3129
|
+
changed: true,
|
|
3130
|
+
workspaceControlEventId
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
async function mutateWorkspaceControlInTransaction(db, input) {
|
|
3134
|
+
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update", {
|
|
3135
|
+
...input.controlLockTimeoutMs !== void 0 ? { lockTimeoutMs: input.controlLockTimeoutMs } : {}
|
|
3136
|
+
});
|
|
3137
|
+
const currentRevision = asSafeRevision(workspace.revision, "workspace control revision");
|
|
3138
|
+
const hash = canonicalSessionCommandHash({
|
|
3139
|
+
action: input.action,
|
|
3140
|
+
reason: input.reason ?? null,
|
|
3141
|
+
expectedRevision: input.expectedRevision ?? null
|
|
3142
|
+
});
|
|
3143
|
+
const reserved = await reserveSessionCommandReceipt(db, {
|
|
3144
|
+
accountId: input.accountId,
|
|
3145
|
+
workspaceId: input.workspaceId,
|
|
3146
|
+
actor: input.actor,
|
|
3147
|
+
action: `workspace.${input.action}`,
|
|
3148
|
+
targetSessionId: null,
|
|
3149
|
+
targetTurnId: null,
|
|
3150
|
+
operationKey: input.operationKey,
|
|
3151
|
+
canonicalRequestHash: hash
|
|
3152
|
+
});
|
|
3153
|
+
if (reserved.replay) {
|
|
3154
|
+
const storedOutcome = reserved.receipt.result.outcome;
|
|
3155
|
+
const unchanged = storedOutcome === "unchanged" && reserved.receipt.appliedControlRevision === null;
|
|
3156
|
+
if (!unchanged && reserved.receipt.appliedControlRevision === null) {
|
|
3157
|
+
throw new SessionControlInvariantError(
|
|
3158
|
+
"Replayed workspace control receipt has no applied control revision"
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
3161
|
+
const workspaceControlEventId2 = unchanged ? null : String(reserved.receipt.result.workspaceControlEventId ?? "");
|
|
3162
|
+
if (!unchanged && !workspaceControlEventId2) {
|
|
3163
|
+
throw new SessionControlInvariantError("Replayed workspace control receipt has no event");
|
|
3164
|
+
}
|
|
3165
|
+
return {
|
|
3166
|
+
receipt: reserved.receipt,
|
|
3167
|
+
revision: reserved.receipt.appliedControlRevision === null ? currentRevision : Number(reserved.receipt.appliedControlRevision),
|
|
3168
|
+
workspaceControlEventId: workspaceControlEventId2 || null,
|
|
3169
|
+
workspaceState: workspace.workspaceState === "paused" ? "paused" : "active",
|
|
3170
|
+
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
3171
|
+
backgroundCommandCount: Number(reserved.receipt.result.backgroundCommandCount ?? 0),
|
|
3172
|
+
wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
|
|
3173
|
+
outcome: "replayed",
|
|
3174
|
+
replay: true
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (input.expectedRevision !== null && input.expectedRevision !== void 0) {
|
|
3178
|
+
if (input.expectedRevision !== currentRevision) throw new SessionControlConflictError();
|
|
3179
|
+
}
|
|
3180
|
+
const workspacePauseRevision = asSafeRevision(
|
|
3181
|
+
workspace.workspacePauseRevision,
|
|
3182
|
+
"workspace pause revision"
|
|
3183
|
+
);
|
|
3184
|
+
const changed = input.action === "pause" ? workspace.workspaceState !== "paused" || workspacePauseRevision === null || await workspacePauseEffectsNeeded(db, {
|
|
3185
|
+
workspaceId: input.workspaceId,
|
|
3186
|
+
workspacePauseRevision
|
|
3187
|
+
}) : workspace.workspaceState !== "active" || await continuableWakeRepairNeeded(db, {
|
|
3188
|
+
workspaceId: input.workspaceId,
|
|
3189
|
+
rootSessionId: null
|
|
3190
|
+
});
|
|
3191
|
+
if (!changed) {
|
|
3192
|
+
const receipt2 = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
3193
|
+
result: {
|
|
3194
|
+
outcome: "unchanged",
|
|
3195
|
+
interruptionCount: 0,
|
|
3196
|
+
backgroundCommandCount: 0,
|
|
3197
|
+
wakeCount: 0
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
return {
|
|
3201
|
+
receipt: receipt2,
|
|
3202
|
+
revision: currentRevision,
|
|
3203
|
+
workspaceControlEventId: null,
|
|
3204
|
+
workspaceState: workspace.workspaceState === "paused" ? "paused" : "active",
|
|
3205
|
+
interruptionCount: 0,
|
|
3206
|
+
backgroundCommandCount: 0,
|
|
3207
|
+
wakeCount: 0,
|
|
3208
|
+
outcome: "unchanged",
|
|
3209
|
+
replay: false
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
const revision = nextRevision(workspace);
|
|
3213
|
+
const actor = input.actor.type === "agent_attempt" ? `attempt:${input.actor.attemptId}` : input.actor.subjectId;
|
|
3214
|
+
const backgroundCommandCount = input.action === "pause" ? await requestWorkspaceBackgroundCommandCancellationsInTransaction(db, {
|
|
3215
|
+
workspaceId: input.workspaceId,
|
|
3216
|
+
subjectId: actor
|
|
3217
|
+
}) : 0;
|
|
3218
|
+
const [updated] = await db.update(workspaceInferenceControls).set({
|
|
3219
|
+
revision,
|
|
3220
|
+
workspaceState: input.action === "pause" ? "paused" : "active",
|
|
3221
|
+
workspacePauseRevision: input.action === "pause" ? revision : null,
|
|
3222
|
+
reason: input.reason ?? null,
|
|
3223
|
+
changedBy: actor,
|
|
3224
|
+
changedAt: /* @__PURE__ */ new Date(),
|
|
3225
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3226
|
+
}).where(
|
|
3227
|
+
and3(
|
|
3228
|
+
eq3(workspaceInferenceControls.workspaceId, input.workspaceId),
|
|
3229
|
+
eq3(workspaceInferenceControls.revision, currentRevision)
|
|
3230
|
+
)
|
|
3231
|
+
).returning({ workspaceId: workspaceInferenceControls.workspaceId });
|
|
3232
|
+
if (!updated) {
|
|
3233
|
+
throw new SessionControlInvariantError("Workspace control did not mutate exactly once");
|
|
3234
|
+
}
|
|
3235
|
+
const workspaceControlEventId = await insertWorkspaceControlEventInTransaction(db, {
|
|
3236
|
+
accountId: input.accountId,
|
|
3237
|
+
workspaceId: input.workspaceId,
|
|
3238
|
+
revision,
|
|
3239
|
+
scope: "workspace",
|
|
3240
|
+
rootSessionId: null,
|
|
3241
|
+
action: input.action,
|
|
3242
|
+
automatic: false,
|
|
3243
|
+
reason: input.reason ?? null,
|
|
3244
|
+
actor
|
|
3245
|
+
});
|
|
3246
|
+
const interruptionCount = input.action === "pause" ? await interruptWorkspaceAttempts(db, {
|
|
3247
|
+
workspaceId: input.workspaceId,
|
|
3248
|
+
operationId: reserved.receipt.id,
|
|
3249
|
+
controlRevision: revision
|
|
3250
|
+
}) : 0;
|
|
3251
|
+
const wakeCount = input.action === "pause" ? await registerInterruptionWakes(db, {
|
|
3252
|
+
operationId: reserved.receipt.id,
|
|
3253
|
+
reason: "workspace_pause_interruption"
|
|
3254
|
+
}) : await registerWorkspaceWakes(db, {
|
|
3255
|
+
workspaceId: input.workspaceId,
|
|
3256
|
+
reason: "workspace_resume"
|
|
3257
|
+
});
|
|
3258
|
+
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
3259
|
+
controlRevision: revision,
|
|
3260
|
+
result: {
|
|
3261
|
+
outcome: "changed",
|
|
3262
|
+
interruptionCount,
|
|
3263
|
+
backgroundCommandCount,
|
|
3264
|
+
wakeCount,
|
|
3265
|
+
workspaceControlEventId
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
return {
|
|
3269
|
+
receipt,
|
|
3270
|
+
revision,
|
|
3271
|
+
workspaceControlEventId,
|
|
3272
|
+
workspaceState: input.action === "pause" ? "paused" : "active",
|
|
3273
|
+
interruptionCount,
|
|
3274
|
+
backgroundCommandCount,
|
|
3275
|
+
wakeCount,
|
|
3276
|
+
outcome: "changed",
|
|
3277
|
+
replay: false
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
async function insertWorkspaceControlEventInTransaction(db, input) {
|
|
3281
|
+
const projected = boundWorkspaceControlEvent(
|
|
3282
|
+
{
|
|
3283
|
+
id: crypto.randomUUID(),
|
|
3284
|
+
workspaceId: input.workspaceId,
|
|
3285
|
+
sequence: input.revision,
|
|
3286
|
+
revision: input.revision,
|
|
3287
|
+
type: "workspace.control.changed",
|
|
3288
|
+
scope: input.scope,
|
|
3289
|
+
rootSessionId: input.rootSessionId,
|
|
3290
|
+
action: input.action,
|
|
3291
|
+
automatic: input.automatic,
|
|
3292
|
+
reason: input.reason,
|
|
3293
|
+
actor: input.actor,
|
|
3294
|
+
occurredAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
3295
|
+
},
|
|
3296
|
+
{ surface: "durable_control" }
|
|
3297
|
+
);
|
|
3298
|
+
const field = (name) => projected.truncation?.fields.find((candidate) => candidate.field === name);
|
|
3299
|
+
const [event] = await db.insert(workspaceControlEvents).values({
|
|
3300
|
+
...input,
|
|
3301
|
+
reason: projected.reason,
|
|
3302
|
+
reasonOriginalBytes: input.reason === null ? null : field("reason")?.originalBytes ?? workspaceControlUtf8Bytes(projected.reason),
|
|
3303
|
+
actor: projected.actor,
|
|
3304
|
+
actorOriginalBytes: field("actor")?.originalBytes ?? workspaceControlUtf8Bytes(projected.actor)
|
|
3305
|
+
}).returning({ id: workspaceControlEvents.id });
|
|
3306
|
+
if (!event) {
|
|
3307
|
+
throw new SessionControlInvariantError("Workspace control event was not inserted");
|
|
3308
|
+
}
|
|
3309
|
+
return event.id;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/session-background-commands.ts
|
|
3313
|
+
var SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES = 512;
|
|
3314
|
+
function boundedSessionBackgroundCommandReason(value, label) {
|
|
3315
|
+
const normalized = value.trim();
|
|
3316
|
+
let bounded = "";
|
|
3317
|
+
let bytes = 0;
|
|
3318
|
+
for (const character of normalized) {
|
|
3319
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
3320
|
+
if (bytes + characterBytes > SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES) break;
|
|
3321
|
+
bounded += character;
|
|
3322
|
+
bytes += characterBytes;
|
|
3323
|
+
}
|
|
3324
|
+
if (!bounded) throw new Error(`${label} must not be empty`);
|
|
3325
|
+
return bounded;
|
|
3326
|
+
}
|
|
3327
|
+
function commandPreview(value) {
|
|
3328
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
3329
|
+
return Array.from(normalized).slice(0, 512).join("");
|
|
3330
|
+
}
|
|
3331
|
+
function mapCommand(row) {
|
|
3332
|
+
const terminal = row.state === "exited" || row.state === "lost";
|
|
3333
|
+
const legacyFailureCode = row.provider === "connected_machine" ? /^op_failure_([A-Za-z0-9_-]{1,128})$/.exec(row.settlementReason ?? "")?.[1] : void 0;
|
|
3334
|
+
const storedFailure = row.runnerFailure ? SessionCommandFailure.safeParse(row.runnerFailure) : null;
|
|
3335
|
+
const failure = !terminal ? void 0 : row.runnerFailure ? storedFailure?.success ? storedFailure.data : {
|
|
3336
|
+
code: /^[A-Za-z0-9_-]{1,128}$/.test(row.runnerFailure.code) ? row.runnerFailure.code : "INVALID_RUNNER_FAILURE",
|
|
3337
|
+
detail: {
|
|
3338
|
+
metadata_error: "Stored runner failure details do not match the retained contract."
|
|
3339
|
+
},
|
|
3340
|
+
retryable: false
|
|
3341
|
+
} : legacyFailureCode ? { code: legacyFailureCode, retryable: false } : void 0;
|
|
3342
|
+
return {
|
|
3343
|
+
id: row.id,
|
|
3344
|
+
workspaceId: row.workspaceId,
|
|
3345
|
+
sessionId: row.sessionId,
|
|
3346
|
+
provider: row.provider,
|
|
3347
|
+
state: row.state,
|
|
3348
|
+
commandPreview: row.commandPreview,
|
|
3349
|
+
cancelRequestedAt: row.cancelRequestedAt?.toISOString() ?? null,
|
|
3350
|
+
exitCode: row.exitCode ?? null,
|
|
3351
|
+
settlementReason: row.settlementReason ?? null,
|
|
3352
|
+
...failure ? { failure } : {},
|
|
3353
|
+
startedAt: row.startedAt.toISOString(),
|
|
3354
|
+
settledAt: row.settledAt?.toISOString() ?? null,
|
|
3355
|
+
completionObservedAt: row.completionObservedAt?.toISOString() ?? null,
|
|
3356
|
+
updatedAt: row.updatedAt.toISOString()
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
async function backgroundCommandActivityForSessions(db, input) {
|
|
3360
|
+
if (input.sessionIds.length === 0) return /* @__PURE__ */ new Map();
|
|
3361
|
+
return await withRlsContext(
|
|
3362
|
+
db,
|
|
3363
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
3364
|
+
async (scopedDb) => {
|
|
3365
|
+
const rows = await scopedDb.select({
|
|
3366
|
+
sessionId: sessionBackgroundCommands.sessionId,
|
|
3367
|
+
count: sql4`count(*)::int`,
|
|
3368
|
+
stoppingCount: sql4`count(*) filter (where ${sessionBackgroundCommands.state} = 'stopping')::int`
|
|
3369
|
+
}).from(sessionBackgroundCommands).where(
|
|
3370
|
+
and4(
|
|
3371
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3372
|
+
inArray3(sessionBackgroundCommands.sessionId, input.sessionIds),
|
|
3373
|
+
inArray3(sessionBackgroundCommands.state, ["running", "stopping"])
|
|
3374
|
+
)
|
|
3375
|
+
).groupBy(sessionBackgroundCommands.sessionId);
|
|
3376
|
+
return new Map(
|
|
3377
|
+
rows.map((row) => [
|
|
3378
|
+
row.sessionId,
|
|
3379
|
+
{
|
|
3380
|
+
state: Number(row.stoppingCount) > 0 ? "stopping" : "running",
|
|
3381
|
+
count: Number(row.count)
|
|
3382
|
+
}
|
|
3383
|
+
])
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
async function listSessionBackgroundCommands(db, input) {
|
|
3389
|
+
return await withRlsContext(
|
|
3390
|
+
db,
|
|
3391
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
3392
|
+
async (scopedDb) => {
|
|
3393
|
+
const rows = await scopedDb.select().from(sessionBackgroundCommands).where(
|
|
3394
|
+
and4(
|
|
3395
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3396
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3397
|
+
...input.activeOnly ? [inArray3(sessionBackgroundCommands.state, ["running", "stopping"])] : []
|
|
3398
|
+
)
|
|
3399
|
+
).orderBy(
|
|
3400
|
+
desc2(sessionBackgroundCommands.startedAt),
|
|
3401
|
+
desc2(sessionBackgroundCommands.id)
|
|
3402
|
+
).limit(1e3);
|
|
3403
|
+
return rows.map(mapCommand);
|
|
3404
|
+
}
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
async function insertManagedSessionBackgroundCommandInTransaction(db, input) {
|
|
3408
|
+
const [process] = await db.select({
|
|
3409
|
+
accountId: sandboxRetainedProcesses.accountId,
|
|
3410
|
+
workspaceId: sandboxRetainedProcesses.workspaceId,
|
|
3411
|
+
sessionId: sandboxRetainedProcesses.sessionId,
|
|
3412
|
+
state: sandboxRetainedProcesses.state,
|
|
3413
|
+
ownerActorKind: sandboxRetainedProcesses.ownerActorKind,
|
|
3414
|
+
ownerActorId: sandboxRetainedProcesses.ownerActorId,
|
|
3415
|
+
ownerTurnId: sandboxRetainedProcesses.ownerTurnId,
|
|
3416
|
+
ownerAttemptId: sandboxRetainedProcesses.ownerAttemptId,
|
|
3417
|
+
ownerExecutionGeneration: sandboxRetainedProcesses.ownerExecutionGeneration
|
|
3418
|
+
}).from(sandboxRetainedProcesses).where(eq4(sandboxRetainedProcesses.id, input.retainedProcessId)).for("update").limit(1);
|
|
3419
|
+
if (!process || process.accountId !== input.accountId || process.workspaceId !== input.workspaceId || process.sessionId !== input.sessionId || process.state !== "active") {
|
|
3420
|
+
throw new Error("Managed background command requires its exact active retained process");
|
|
3421
|
+
}
|
|
3422
|
+
const [row] = await db.insert(sessionBackgroundCommands).values({
|
|
3423
|
+
id: input.commandId,
|
|
3424
|
+
accountId: input.accountId,
|
|
3425
|
+
workspaceId: input.workspaceId,
|
|
3426
|
+
sessionId: input.sessionId,
|
|
3427
|
+
provider: "managed",
|
|
3428
|
+
state: "running",
|
|
3429
|
+
retainedProcessId: input.retainedProcessId,
|
|
3430
|
+
commandPreview: commandPreview(input.command),
|
|
3431
|
+
launchTurnId: input.turnId ?? null,
|
|
3432
|
+
launchAttemptId: input.attemptId ?? null,
|
|
3433
|
+
launchExecutionGeneration: input.executionGeneration ?? null
|
|
3434
|
+
}).onConflictDoUpdate({
|
|
3435
|
+
target: sessionBackgroundCommands.retainedProcessId,
|
|
3436
|
+
targetWhere: sql4`${sessionBackgroundCommands.retainedProcessId} is not null`,
|
|
3437
|
+
set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
3438
|
+
}).returning();
|
|
3439
|
+
if (!row) throw new Error("Managed background command adoption returned no row");
|
|
3440
|
+
const legacyReplayMatches = row.launchTurnId === null && row.launchAttemptId === null && row.launchExecutionGeneration === null && process.ownerActorKind === "turn" && process.ownerActorId === input.attemptId && process.ownerTurnId === input.turnId && process.ownerAttemptId === input.attemptId && process.ownerExecutionGeneration === input.executionGeneration;
|
|
3441
|
+
if (row.accountId !== input.accountId || row.workspaceId !== input.workspaceId || row.sessionId !== input.sessionId || !legacyReplayMatches && (row.launchTurnId !== (input.turnId ?? null) || row.launchAttemptId !== (input.attemptId ?? null) || row.launchExecutionGeneration !== (input.executionGeneration ?? null)) || row.provider !== "managed" || row.retainedProcessId !== input.retainedProcessId) {
|
|
3442
|
+
throw new Error("Managed background command adoption conflicted with another identity");
|
|
3443
|
+
}
|
|
3444
|
+
return mapCommand(row);
|
|
3445
|
+
}
|
|
3446
|
+
async function insertConnectedMachineSessionBackgroundCommandInTransaction(db, input) {
|
|
3447
|
+
const [row] = await db.insert(sessionBackgroundCommands).values({
|
|
3448
|
+
id: input.commandId,
|
|
3449
|
+
accountId: input.accountId,
|
|
3450
|
+
workspaceId: input.workspaceId,
|
|
3451
|
+
sessionId: input.sessionId,
|
|
3452
|
+
provider: "connected_machine",
|
|
3453
|
+
state: "running",
|
|
3454
|
+
controlWorkspaceId: input.controlWorkspaceId,
|
|
3455
|
+
enrollmentId: input.enrollmentId,
|
|
3456
|
+
connectionInstanceId: input.connectionInstanceId,
|
|
3457
|
+
opId: input.opId,
|
|
3458
|
+
commandPreview: commandPreview(input.command),
|
|
3459
|
+
launchTurnId: input.turnId ?? null,
|
|
3460
|
+
launchAttemptId: input.attemptId ?? null,
|
|
3461
|
+
launchExecutionGeneration: input.executionGeneration ?? null
|
|
3462
|
+
}).onConflictDoUpdate({
|
|
3463
|
+
target: [
|
|
3464
|
+
sessionBackgroundCommands.controlWorkspaceId,
|
|
3465
|
+
sessionBackgroundCommands.enrollmentId,
|
|
3466
|
+
sessionBackgroundCommands.connectionInstanceId,
|
|
3467
|
+
sessionBackgroundCommands.opId
|
|
3468
|
+
],
|
|
3469
|
+
targetWhere: sql4`${sessionBackgroundCommands.provider} = 'connected_machine'`,
|
|
3470
|
+
set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
3471
|
+
}).returning();
|
|
3472
|
+
if (!row) throw new Error("Connected Machine background command adoption returned no row");
|
|
3473
|
+
if (row.accountId !== input.accountId || row.workspaceId !== input.workspaceId || row.sessionId !== input.sessionId || row.launchTurnId !== (input.turnId ?? null) || row.launchAttemptId !== (input.attemptId ?? null) || row.launchExecutionGeneration !== (input.executionGeneration ?? null) || row.provider !== "connected_machine" || row.controlWorkspaceId !== input.controlWorkspaceId || row.enrollmentId !== input.enrollmentId || row.connectionInstanceId !== input.connectionInstanceId || row.opId !== input.opId) {
|
|
3474
|
+
throw new Error(
|
|
3475
|
+
"Connected Machine background command adoption conflicted with another identity"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
return mapCommand(row);
|
|
3479
|
+
}
|
|
3480
|
+
async function claimConnectedMachineSessionBackgroundCommands(db, input) {
|
|
3481
|
+
if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100) {
|
|
3482
|
+
throw new Error("Connected command reconciliation limit must be between 1 and 100");
|
|
3483
|
+
}
|
|
3484
|
+
if (!Number.isSafeInteger(input.claimTtlMs) || input.claimTtlMs < 0 || input.claimTtlMs > 36e5) {
|
|
3485
|
+
throw new Error("Connected command reconciliation claim TTL is invalid");
|
|
3486
|
+
}
|
|
3487
|
+
const rows = await db.execute(sql4`
|
|
3488
|
+
select account_id as "accountId",
|
|
3489
|
+
workspace_id as "workspaceId",
|
|
3490
|
+
session_id as "sessionId",
|
|
3491
|
+
command_id as "commandId",
|
|
3492
|
+
claim_id as "claimId",
|
|
3493
|
+
command_state as "commandState",
|
|
3494
|
+
control_workspace_id as "controlWorkspaceId",
|
|
3495
|
+
enrollment_id as "enrollmentId",
|
|
3496
|
+
connection_instance_id as "connectionInstanceId",
|
|
3497
|
+
op_id as "opId",
|
|
3498
|
+
reconcile_attempts as "reconcileAttempts",
|
|
3499
|
+
reconcile_proof_outcome as "reconcileProofOutcome",
|
|
3500
|
+
reconcile_proof_exit_code as "reconcileProofExitCode",
|
|
3501
|
+
reconcile_proof_reason as "reconcileProofReason",
|
|
3502
|
+
reconcile_proof_observed_at as "reconcileProofObservedAt"
|
|
3503
|
+
from opengeni_private.claim_connected_machine_background_commands(
|
|
3504
|
+
${input.claimId}::uuid, ${input.limit}::integer, ${input.claimTtlMs}::bigint,
|
|
3505
|
+
${(input.dueBefore ?? /* @__PURE__ */ new Date()).toISOString()}::timestamptz
|
|
3506
|
+
)
|
|
3507
|
+
`);
|
|
3508
|
+
return rows.map((row) => {
|
|
3509
|
+
if (row.commandState !== "running" && row.commandState !== "stopping") {
|
|
3510
|
+
throw new Error(`Connected command claim returned invalid state ${row.commandState}`);
|
|
3511
|
+
}
|
|
3512
|
+
const proof = row.reconcileProofOutcome === null ? null : row.reconcileProofOutcome === "exited" || row.reconcileProofOutcome === "lost" ? {
|
|
3513
|
+
outcome: row.reconcileProofOutcome,
|
|
3514
|
+
exitCode: row.reconcileProofExitCode === null ? null : Number(row.reconcileProofExitCode),
|
|
3515
|
+
reason: row.reconcileProofReason,
|
|
3516
|
+
observedAt: row.reconcileProofObservedAt instanceof Date ? row.reconcileProofObservedAt : new Date(row.reconcileProofObservedAt)
|
|
3517
|
+
} : (() => {
|
|
3518
|
+
throw new Error(
|
|
3519
|
+
`Connected command claim returned invalid proof ${row.reconcileProofOutcome}`
|
|
3520
|
+
);
|
|
3521
|
+
})();
|
|
3522
|
+
return {
|
|
3523
|
+
commandId: row.commandId,
|
|
3524
|
+
accountId: row.accountId,
|
|
3525
|
+
workspaceId: row.workspaceId,
|
|
3526
|
+
sessionId: row.sessionId,
|
|
3527
|
+
claimId: row.claimId,
|
|
3528
|
+
state: row.commandState,
|
|
3529
|
+
controlWorkspaceId: row.controlWorkspaceId,
|
|
3530
|
+
enrollmentId: row.enrollmentId,
|
|
3531
|
+
connectionInstanceId: row.connectionInstanceId,
|
|
3532
|
+
opId: row.opId,
|
|
3533
|
+
reconcileAttempts: Number(row.reconcileAttempts),
|
|
3534
|
+
proof
|
|
3535
|
+
};
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
function connectedClaimIdentityWhere(claim) {
|
|
3539
|
+
return and4(
|
|
3540
|
+
eq4(sessionBackgroundCommands.id, claim.commandId),
|
|
3541
|
+
eq4(sessionBackgroundCommands.accountId, claim.accountId),
|
|
3542
|
+
eq4(sessionBackgroundCommands.workspaceId, claim.workspaceId),
|
|
3543
|
+
eq4(sessionBackgroundCommands.sessionId, claim.sessionId),
|
|
3544
|
+
eq4(sessionBackgroundCommands.provider, "connected_machine"),
|
|
3545
|
+
eq4(sessionBackgroundCommands.controlWorkspaceId, claim.controlWorkspaceId),
|
|
3546
|
+
eq4(sessionBackgroundCommands.enrollmentId, claim.enrollmentId),
|
|
3547
|
+
eq4(sessionBackgroundCommands.connectionInstanceId, claim.connectionInstanceId),
|
|
3548
|
+
eq4(sessionBackgroundCommands.opId, claim.opId),
|
|
3549
|
+
eq4(sessionBackgroundCommands.reconcileClaimId, claim.claimId),
|
|
3550
|
+
inArray3(sessionBackgroundCommands.state, ["running", "stopping"])
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
async function recordConnectedMachineBackgroundCommandProof(db, input) {
|
|
3554
|
+
const failure = input.proof.failure ? SessionCommandFailure.parse(input.proof.failure) : null;
|
|
3555
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
3556
|
+
input.proof.reason,
|
|
3557
|
+
"Connected command proof reason"
|
|
3558
|
+
);
|
|
3559
|
+
if (failure && reason !== `op_failure_${failure.code}`)
|
|
3560
|
+
throw new Error("Connected command failure code conflicts with proof reason");
|
|
3561
|
+
if (input.proof.outcome === "exited" && input.proof.exitCode === null) {
|
|
3562
|
+
throw new Error("Connected command exit proof requires an exit code");
|
|
3563
|
+
}
|
|
3564
|
+
if (input.proof.outcome === "lost" && input.proof.exitCode !== null) {
|
|
3565
|
+
throw new Error("Connected command loss proof cannot carry an exit code");
|
|
3566
|
+
}
|
|
3567
|
+
await withRlsContext(
|
|
3568
|
+
db,
|
|
3569
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
3570
|
+
async (scopedDb) => {
|
|
3571
|
+
const [current] = await scopedDb.select().from(sessionBackgroundCommands).where(connectedClaimIdentityWhere(input.claim)).for("update").limit(1);
|
|
3572
|
+
if (!current) throw new Error("Connected command proof was fenced by newer lifecycle state");
|
|
3573
|
+
if (current.reconcileProofOutcome !== null) {
|
|
3574
|
+
const observedAt = current.reconcileProofObservedAt?.getTime() ?? null;
|
|
3575
|
+
if (current.reconcileProofOutcome !== input.proof.outcome || current.reconcileProofExitCode !== input.proof.exitCode || current.reconcileProofReason !== reason || !isDeepStrictEqual(current.runnerFailure, failure) || observedAt !== input.proof.observedAt.getTime()) {
|
|
3576
|
+
throw new Error("Connected command reconciliation proof conflicts with durable proof");
|
|
3577
|
+
}
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
const rows = await scopedDb.update(sessionBackgroundCommands).set({
|
|
3581
|
+
reconcileProofOutcome: input.proof.outcome,
|
|
3582
|
+
reconcileProofExitCode: input.proof.exitCode,
|
|
3583
|
+
reconcileProofReason: reason,
|
|
3584
|
+
runnerFailure: failure,
|
|
3585
|
+
reconcileProofObservedAt: input.proof.observedAt,
|
|
3586
|
+
lastReconcileOutcome: `proof_${input.proof.outcome}`,
|
|
3587
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3588
|
+
}).where(connectedClaimIdentityWhere(input.claim)).returning({ id: sessionBackgroundCommands.id });
|
|
3589
|
+
if (rows.length !== 1) {
|
|
3590
|
+
throw new Error("Connected command proof lost its exact claim during checkpoint");
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
);
|
|
3594
|
+
}
|
|
3595
|
+
async function deferConnectedMachineBackgroundCommandReconciliation(db, input) {
|
|
3596
|
+
if (!Number.isSafeInteger(input.retryAfterMs) || input.retryAfterMs < 0) {
|
|
3597
|
+
throw new Error("Connected command reconciliation retry delay is invalid");
|
|
3598
|
+
}
|
|
3599
|
+
const outcome = input.outcome.trim().slice(0, 64);
|
|
3600
|
+
if (!outcome) throw new Error("Connected command reconciliation outcome must not be empty");
|
|
3601
|
+
return await withRlsContext(
|
|
3602
|
+
db,
|
|
3603
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
3604
|
+
async (scopedDb) => {
|
|
3605
|
+
const rows = await scopedDb.update(sessionBackgroundCommands).set({
|
|
3606
|
+
reconcileAfter: new Date(Date.now() + input.retryAfterMs),
|
|
3607
|
+
reconcileClaimId: null,
|
|
3608
|
+
reconcileClaimedAt: null,
|
|
3609
|
+
lastReconcileOutcome: outcome,
|
|
3610
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3611
|
+
}).where(connectedClaimIdentityWhere(input.claim)).returning({ id: sessionBackgroundCommands.id });
|
|
3612
|
+
return rows.length === 1;
|
|
3613
|
+
}
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
async function settleClaimedConnectedMachineBackgroundCommandWithMutation(db, input, mutateTerminal) {
|
|
3617
|
+
return await withSessionActivityRlsContext(
|
|
3618
|
+
db,
|
|
3619
|
+
{ accountId: input.claim.accountId, workspaceId: input.claim.workspaceId },
|
|
3620
|
+
async (tx) => {
|
|
3621
|
+
await mutateTerminal.prepare(tx);
|
|
3622
|
+
const rows = await tx.execute(sql4`
|
|
3623
|
+
update ${sessionBackgroundCommands} command set
|
|
3624
|
+
state = command.reconcile_proof_outcome,
|
|
3625
|
+
exit_code = case
|
|
3626
|
+
when command.reconcile_proof_outcome = 'exited'
|
|
3627
|
+
then command.reconcile_proof_exit_code
|
|
3628
|
+
else null
|
|
3629
|
+
end,
|
|
3630
|
+
settlement_reason = command.reconcile_proof_reason,
|
|
3631
|
+
settled_at = command.reconcile_proof_observed_at,
|
|
3632
|
+
reconcile_claim_id = null,
|
|
3633
|
+
reconcile_claimed_at = null,
|
|
3634
|
+
last_reconcile_outcome = 'settled_' || command.reconcile_proof_outcome,
|
|
3635
|
+
updated_at = clock_timestamp()
|
|
3636
|
+
where command.id = ${input.claim.commandId}
|
|
3637
|
+
and command.account_id = ${input.claim.accountId}
|
|
3638
|
+
and command.workspace_id = ${input.claim.workspaceId}
|
|
3639
|
+
and command.session_id = ${input.claim.sessionId}
|
|
3640
|
+
and command.provider = 'connected_machine'
|
|
3641
|
+
and command.control_workspace_id = ${input.claim.controlWorkspaceId}
|
|
3642
|
+
and command.enrollment_id = ${input.claim.enrollmentId}
|
|
3643
|
+
and command.connection_instance_id = ${input.claim.connectionInstanceId}
|
|
3644
|
+
and command.op_id = ${input.claim.opId}
|
|
3645
|
+
and command.reconcile_claim_id = ${input.claim.claimId}
|
|
3646
|
+
and command.state in ('running', 'stopping')
|
|
3647
|
+
and command.reconcile_proof_outcome in ('exited', 'lost')
|
|
3648
|
+
and command.reconcile_proof_observed_at is not null
|
|
3649
|
+
returning command.id
|
|
3650
|
+
`);
|
|
3651
|
+
if (rows.length !== 1) return false;
|
|
3652
|
+
const [row] = await tx.select().from(sessionBackgroundCommands).where(eq4(sessionBackgroundCommands.id, input.claim.commandId)).limit(1);
|
|
3653
|
+
if (!row) throw new Error("Settled Connected Machine command disappeared");
|
|
3654
|
+
await mutateTerminal.commit(tx, mapCommand(row));
|
|
3655
|
+
return true;
|
|
3656
|
+
}
|
|
3657
|
+
);
|
|
3658
|
+
}
|
|
3659
|
+
async function requestSessionBackgroundCommandCancellation(db, input) {
|
|
3660
|
+
return await withRlsContext(
|
|
3661
|
+
db,
|
|
3662
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
3663
|
+
async (scopedDb) => await scopedDb.transaction(async (tx) => {
|
|
3664
|
+
const [current] = await tx.select().from(sessionBackgroundCommands).where(
|
|
3665
|
+
and4(
|
|
3666
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3667
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3668
|
+
eq4(sessionBackgroundCommands.id, input.commandId)
|
|
3669
|
+
)
|
|
3670
|
+
).for("update").limit(1);
|
|
3671
|
+
if (!current) return { command: null, accepted: false };
|
|
3672
|
+
if (current.state !== "running") {
|
|
3673
|
+
return { command: mapCommand(current), accepted: false };
|
|
3674
|
+
}
|
|
3675
|
+
const [updated] = await tx.update(sessionBackgroundCommands).set({
|
|
3676
|
+
state: "stopping",
|
|
3677
|
+
cancelRequestedAt: /* @__PURE__ */ new Date(),
|
|
3678
|
+
cancelRequestedBy: input.subjectId.slice(0, 1024),
|
|
3679
|
+
reconcileAfter: /* @__PURE__ */ new Date(),
|
|
3680
|
+
reconcileClaimId: null,
|
|
3681
|
+
reconcileClaimedAt: null,
|
|
3682
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3683
|
+
}).where(eq4(sessionBackgroundCommands.id, current.id)).returning();
|
|
3684
|
+
if (current.retainedProcessId) {
|
|
3685
|
+
await tx.update(sandboxRetainedProcesses).set({ reconcileAfter: /* @__PURE__ */ new Date(), lastReconcileOutcome: "cancel_requested" }).where(
|
|
3686
|
+
and4(
|
|
3687
|
+
eq4(sandboxRetainedProcesses.id, current.retainedProcessId),
|
|
3688
|
+
eq4(sandboxRetainedProcesses.state, "active"),
|
|
3689
|
+
sql4`${sandboxRetainedProcesses.reconcileClaimId} is null`
|
|
3690
|
+
)
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
return { command: mapCommand(updated), accepted: true };
|
|
3694
|
+
})
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
async function requestSessionBackgroundCommandCancellationsInTransaction(db, input) {
|
|
3698
|
+
const rootSessionIds = [...new Set(input.rootSessionIds)];
|
|
3699
|
+
if (rootSessionIds.length === 0) return 0;
|
|
3700
|
+
const rows = await db.execute(sql4`
|
|
3701
|
+
with recursive roots(id) as (
|
|
3702
|
+
values ${sql4.join(
|
|
3703
|
+
rootSessionIds.map((sessionId) => sql4`(${sessionId}::uuid)`),
|
|
3704
|
+
sql4`, `
|
|
3705
|
+
)}
|
|
3706
|
+
), subtree(id, path, cycle) as (
|
|
3707
|
+
select session.id, array[session.id]::uuid[], false
|
|
3708
|
+
from roots root
|
|
3709
|
+
join ${sessions} session
|
|
3710
|
+
on session.workspace_id = ${input.workspaceId}
|
|
3711
|
+
and session.id = root.id
|
|
3712
|
+
union all
|
|
3713
|
+
select child.id, parent.path || child.id, child.id = any(parent.path)
|
|
3714
|
+
from subtree parent
|
|
3715
|
+
join ${sessions} child
|
|
3716
|
+
on child.workspace_id = ${input.workspaceId}
|
|
3717
|
+
and child.parent_session_id = parent.id
|
|
3718
|
+
where not parent.cycle
|
|
3719
|
+
), stopped as (
|
|
3720
|
+
update ${sessionBackgroundCommands} command set
|
|
3721
|
+
state = 'stopping',
|
|
3722
|
+
cancel_requested_at = clock_timestamp(),
|
|
3723
|
+
cancel_requested_by = left(${input.subjectId}, 1024),
|
|
3724
|
+
reconcile_after = clock_timestamp(),
|
|
3725
|
+
reconcile_claim_id = null,
|
|
3726
|
+
reconcile_claimed_at = null,
|
|
3727
|
+
updated_at = clock_timestamp()
|
|
3728
|
+
where command.workspace_id = ${input.workspaceId}
|
|
3729
|
+
and command.session_id in (select id from subtree where not cycle)
|
|
3730
|
+
and command.state = 'running'
|
|
3731
|
+
returning command.retained_process_id
|
|
3732
|
+
), nudged as (
|
|
3733
|
+
update ${sandboxRetainedProcesses} process set
|
|
3734
|
+
reconcile_after = clock_timestamp(),
|
|
3735
|
+
reconcile_claim_id = null,
|
|
3736
|
+
reconcile_claimed_at = null,
|
|
3737
|
+
last_reconcile_outcome = 'cancel_requested'
|
|
3738
|
+
where process.id in (
|
|
3739
|
+
select stopped.retained_process_id
|
|
3740
|
+
from stopped
|
|
3741
|
+
where stopped.retained_process_id is not null
|
|
3742
|
+
)
|
|
3743
|
+
and process.state = 'active'
|
|
3744
|
+
returning process.id
|
|
3745
|
+
)
|
|
3746
|
+
select count(*)::integer as "commandCount" from stopped
|
|
3747
|
+
`);
|
|
3748
|
+
return Number(rows[0]?.commandCount ?? 0);
|
|
3749
|
+
}
|
|
3750
|
+
async function requestWorkspaceBackgroundCommandCancellationsInTransaction(db, input) {
|
|
3751
|
+
const rows = await db.execute(sql4`
|
|
3752
|
+
with stopped as (
|
|
3753
|
+
update ${sessionBackgroundCommands} command set
|
|
3754
|
+
state = 'stopping',
|
|
3755
|
+
cancel_requested_at = clock_timestamp(),
|
|
3756
|
+
cancel_requested_by = left(${input.subjectId}, 1024),
|
|
3757
|
+
reconcile_after = clock_timestamp(),
|
|
3758
|
+
reconcile_claim_id = null,
|
|
3759
|
+
reconcile_claimed_at = null,
|
|
3760
|
+
updated_at = clock_timestamp()
|
|
3761
|
+
where command.workspace_id = ${input.workspaceId}
|
|
3762
|
+
and command.state = 'running'
|
|
3763
|
+
returning command.retained_process_id
|
|
3764
|
+
), nudged as (
|
|
3765
|
+
update ${sandboxRetainedProcesses} process set
|
|
3766
|
+
reconcile_after = clock_timestamp(),
|
|
3767
|
+
reconcile_claim_id = null,
|
|
3768
|
+
reconcile_claimed_at = null,
|
|
3769
|
+
last_reconcile_outcome = 'cancel_requested'
|
|
3770
|
+
where process.id in (
|
|
3771
|
+
select stopped.retained_process_id
|
|
3772
|
+
from stopped
|
|
3773
|
+
where stopped.retained_process_id is not null
|
|
3774
|
+
)
|
|
3775
|
+
and process.state = 'active'
|
|
3776
|
+
returning process.id
|
|
3777
|
+
)
|
|
3778
|
+
select count(*)::integer as "commandCount" from stopped
|
|
3779
|
+
`);
|
|
3780
|
+
return Number(rows[0]?.commandCount ?? 0);
|
|
3781
|
+
}
|
|
3782
|
+
async function settleSessionBackgroundCommandForRetainedProcessInTransaction(tx, input, mutateTerminal) {
|
|
3783
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
3784
|
+
input.reason,
|
|
3785
|
+
"Managed command settlement reason"
|
|
3786
|
+
);
|
|
3787
|
+
await mutateTerminal.prepare(tx);
|
|
3788
|
+
const [updatedRow] = await tx.update(sessionBackgroundCommands).set({
|
|
3789
|
+
state: input.outcome,
|
|
3790
|
+
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
3791
|
+
settlementReason: reason,
|
|
3792
|
+
settledAt: /* @__PURE__ */ new Date(),
|
|
3793
|
+
reconcileClaimId: null,
|
|
3794
|
+
reconcileClaimedAt: null,
|
|
3795
|
+
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
3796
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3797
|
+
}).where(
|
|
3798
|
+
and4(
|
|
3799
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3800
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3801
|
+
eq4(sessionBackgroundCommands.retainedProcessId, input.retainedProcessId),
|
|
3802
|
+
inArray3(sessionBackgroundCommands.state, ["running", "stopping"])
|
|
3803
|
+
)
|
|
3804
|
+
).returning();
|
|
3805
|
+
let row = updatedRow;
|
|
3806
|
+
if (!row) {
|
|
3807
|
+
const [current] = await tx.select().from(sessionBackgroundCommands).where(
|
|
3808
|
+
and4(
|
|
3809
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3810
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3811
|
+
eq4(sessionBackgroundCommands.retainedProcessId, input.retainedProcessId)
|
|
3812
|
+
)
|
|
3813
|
+
).for("update").limit(1);
|
|
3814
|
+
if (!current) return null;
|
|
3815
|
+
if (current.state !== input.outcome || current.exitCode !== (input.outcome === "exited" ? input.exitCode : null)) {
|
|
3816
|
+
throw new Error("Managed background command terminal state conflicts with process proof");
|
|
3817
|
+
}
|
|
3818
|
+
const [existingEvent] = await tx.select({ id: sessionEvents.id }).from(sessionEvents).where(
|
|
3819
|
+
and4(
|
|
3820
|
+
eq4(sessionEvents.workspaceId, input.workspaceId),
|
|
3821
|
+
eq4(sessionEvents.sessionId, input.sessionId),
|
|
3822
|
+
eq4(sessionEvents.type, "session.command.finished"),
|
|
3823
|
+
sql4`${sessionEvents.payload} ->> 'commandId' = ${current.id}`
|
|
3824
|
+
)
|
|
3825
|
+
).limit(1);
|
|
3826
|
+
if (existingEvent) return mapCommand(current);
|
|
3827
|
+
row = current;
|
|
3828
|
+
}
|
|
3829
|
+
const command = mapCommand(row);
|
|
3830
|
+
await mutateTerminal.commit(tx, command);
|
|
3831
|
+
return command;
|
|
3832
|
+
}
|
|
3833
|
+
async function settleConnectedMachineSessionBackgroundCommandWithMutation(db, input, mutateTerminal) {
|
|
3834
|
+
const failure = input.failure ? SessionCommandFailure.parse(input.failure) : null;
|
|
3835
|
+
const reason = boundedSessionBackgroundCommandReason(
|
|
3836
|
+
input.reason,
|
|
3837
|
+
"Connected command settlement reason"
|
|
3838
|
+
);
|
|
3839
|
+
if (failure && reason !== `op_failure_${failure.code}`)
|
|
3840
|
+
throw new Error("Connected command failure code conflicts with settlement reason");
|
|
3841
|
+
if (input.outcome === "exited" && input.exitCode === null) {
|
|
3842
|
+
throw new Error("Connected command exit settlement requires an exit code");
|
|
3843
|
+
}
|
|
3844
|
+
if (input.outcome === "lost" && input.exitCode !== null) {
|
|
3845
|
+
throw new Error("Connected command loss settlement cannot carry an exit code");
|
|
3846
|
+
}
|
|
3847
|
+
return await withSessionActivityRlsContext(
|
|
3848
|
+
db,
|
|
3849
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
3850
|
+
async (tx) => {
|
|
3851
|
+
await mutateTerminal.prepare(tx);
|
|
3852
|
+
const settledAt = /* @__PURE__ */ new Date();
|
|
3853
|
+
const [row] = await tx.update(sessionBackgroundCommands).set({
|
|
3854
|
+
state: input.outcome,
|
|
3855
|
+
exitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
3856
|
+
settlementReason: reason,
|
|
3857
|
+
runnerFailure: failure,
|
|
3858
|
+
settledAt,
|
|
3859
|
+
reconcileClaimId: null,
|
|
3860
|
+
reconcileClaimedAt: null,
|
|
3861
|
+
reconcileProofOutcome: input.outcome,
|
|
3862
|
+
reconcileProofExitCode: input.outcome === "exited" ? input.exitCode : null,
|
|
3863
|
+
reconcileProofReason: reason,
|
|
3864
|
+
reconcileProofObservedAt: settledAt,
|
|
3865
|
+
lastReconcileOutcome: `settled_${input.outcome}`,
|
|
3866
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3867
|
+
}).where(
|
|
3868
|
+
and4(
|
|
3869
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3870
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3871
|
+
eq4(sessionBackgroundCommands.id, input.commandId),
|
|
3872
|
+
eq4(sessionBackgroundCommands.provider, "connected_machine"),
|
|
3873
|
+
eq4(sessionBackgroundCommands.controlWorkspaceId, input.controlWorkspaceId),
|
|
3874
|
+
eq4(sessionBackgroundCommands.enrollmentId, input.enrollmentId),
|
|
3875
|
+
eq4(sessionBackgroundCommands.connectionInstanceId, input.connectionInstanceId),
|
|
3876
|
+
eq4(sessionBackgroundCommands.opId, input.opId),
|
|
3877
|
+
// A fast owner settlement cannot erase or replace metadata already
|
|
3878
|
+
// checkpointed by the reconciler under the exact command identity.
|
|
3879
|
+
sql4`(${sessionBackgroundCommands.runnerFailure} is null or
|
|
3880
|
+
${sessionBackgroundCommands.runnerFailure} = ${failure ? JSON.stringify(failure) : null}::jsonb)`,
|
|
3881
|
+
inArray3(sessionBackgroundCommands.state, ["running", "stopping"])
|
|
3882
|
+
)
|
|
3883
|
+
).returning();
|
|
3884
|
+
if (!row) return null;
|
|
3885
|
+
const command = mapCommand(row);
|
|
3886
|
+
await mutateTerminal.commit(tx, command);
|
|
3887
|
+
return command;
|
|
3888
|
+
}
|
|
3889
|
+
);
|
|
3890
|
+
}
|
|
3891
|
+
async function getSessionBackgroundCommand(db, input) {
|
|
3892
|
+
return await withRlsContext(
|
|
3893
|
+
db,
|
|
3894
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
3895
|
+
async (scopedDb) => {
|
|
3896
|
+
const [row] = await scopedDb.select().from(sessionBackgroundCommands).where(
|
|
3897
|
+
and4(
|
|
3898
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3899
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3900
|
+
eq4(sessionBackgroundCommands.id, input.commandId)
|
|
3901
|
+
)
|
|
3902
|
+
).limit(1);
|
|
3903
|
+
return row ? mapCommand(row) : null;
|
|
3904
|
+
}
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
async function observeSessionBackgroundCommandCompletion(db, input) {
|
|
3908
|
+
return await withSessionActivityRlsContext(db, input, async (tx) => {
|
|
3909
|
+
const locks = await lockSessionEventWriteRows(tx, {
|
|
3910
|
+
workspaceId: input.workspaceId,
|
|
3911
|
+
controlLock: "share",
|
|
3912
|
+
sessionIds: [input.sessionId]
|
|
3913
|
+
});
|
|
3914
|
+
if (locks.sessions[0]?.accountId !== input.accountId) return null;
|
|
3915
|
+
const identity = and4(
|
|
3916
|
+
eq4(sessionBackgroundCommands.accountId, input.accountId),
|
|
3917
|
+
eq4(sessionBackgroundCommands.workspaceId, input.workspaceId),
|
|
3918
|
+
eq4(sessionBackgroundCommands.sessionId, input.sessionId),
|
|
3919
|
+
eq4(sessionBackgroundCommands.id, input.commandId)
|
|
3920
|
+
);
|
|
3921
|
+
const [command] = await tx.select().from(sessionBackgroundCommands).where(identity).for("update").limit(1);
|
|
3922
|
+
if (!command) return null;
|
|
3923
|
+
if (command.state !== "exited" && command.state !== "lost") return mapCommand(command);
|
|
3924
|
+
const observedAt = command.completionObservedAt ?? /* @__PURE__ */ new Date();
|
|
3925
|
+
await tx.update(sessionBackgroundCommands).set({ completionObservedAt: observedAt }).where(identity);
|
|
3926
|
+
await tx.update(sessionSystemUpdates).set({ state: "superseded" }).where(
|
|
3927
|
+
and4(
|
|
3928
|
+
eq4(sessionSystemUpdates.accountId, input.accountId),
|
|
3929
|
+
eq4(sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
3930
|
+
eq4(sessionSystemUpdates.sessionId, input.sessionId),
|
|
3931
|
+
eq4(sessionSystemUpdates.kind, "background_command_result"),
|
|
3932
|
+
eq4(sessionSystemUpdates.sourceId, input.commandId),
|
|
3933
|
+
eq4(sessionSystemUpdates.dedupeKey, `background-command-result:${input.commandId}`),
|
|
3934
|
+
eq4(sessionSystemUpdates.state, "pending")
|
|
3935
|
+
)
|
|
3936
|
+
);
|
|
3937
|
+
return mapCommand({ ...command, completionObservedAt: observedAt });
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
var COMMAND_OUTPUT_DEFAULT_BYTES = 16384;
|
|
3941
|
+
var COMMAND_OUTPUT_MAX_BYTES = 65536;
|
|
3942
|
+
var COMMAND_OUTPUT_PAGE_ROWS = 64;
|
|
3943
|
+
function parseCommandOutputCursor(cursor, commandId) {
|
|
3944
|
+
if (cursor === void 0) return { sequence: 0, offset: 0 };
|
|
3945
|
+
if (cursor.length > 128) throw new Error("Invalid command output cursor");
|
|
3946
|
+
const parts = cursor.split(":");
|
|
3947
|
+
if (parts.length !== 3 || parts[0] !== commandId || !/^\d+$/.test(parts[1]) || !/^\d+$/.test(parts[2])) {
|
|
3948
|
+
throw new Error("Invalid command output cursor");
|
|
3949
|
+
}
|
|
3950
|
+
const sequence = Number(parts[1]);
|
|
3951
|
+
const offset = Number(parts[2]);
|
|
3952
|
+
if (!Number.isSafeInteger(sequence) || !Number.isSafeInteger(offset))
|
|
3953
|
+
throw new Error("Invalid command output cursor");
|
|
3954
|
+
return { sequence, offset };
|
|
3955
|
+
}
|
|
3956
|
+
function projectCommandOutputPage(input) {
|
|
3957
|
+
const start = parseCommandOutputCursor(input.cursor, input.commandId);
|
|
3958
|
+
const budget = input.maxOutputBytes ?? COMMAND_OUTPUT_DEFAULT_BYTES;
|
|
3959
|
+
if (!Number.isSafeInteger(budget) || budget < 4 || budget > COMMAND_OUTPUT_MAX_BYTES)
|
|
3960
|
+
throw new Error("maxOutputBytes must be between 4 and 65536");
|
|
3961
|
+
let remaining = budget;
|
|
3962
|
+
let sequence = start.sequence;
|
|
3963
|
+
let offset = start.offset;
|
|
3964
|
+
const chunks = [];
|
|
3965
|
+
const gaps = [];
|
|
3966
|
+
if (start.offset > 0 && input.rows[0]?.sequence !== start.sequence)
|
|
3967
|
+
gaps.push("cursor_output_no_longer_retained");
|
|
3968
|
+
let consumed = 0;
|
|
3969
|
+
for (const row of input.rows.slice(0, COMMAND_OUTPUT_PAGE_ROWS)) {
|
|
3970
|
+
const payload = fromPostgresLosslessJson(row.payload, row.payloadCodecVersion);
|
|
3971
|
+
const text = typeof payload?.chunk === "string" ? payload.chunk : "";
|
|
3972
|
+
if (typeof payload?.chunk !== "string") gaps.push("retained_event_has_no_output_chunk");
|
|
3973
|
+
if (payload?.commandReadProjectionGap === true) gaps.push("retained_event_exceeds_read_limit");
|
|
3974
|
+
if (payload?.truncation && typeof payload.truncation === "object")
|
|
3975
|
+
gaps.push("output_truncated_at_retention_boundary");
|
|
3976
|
+
const begin = row.sequence === start.sequence ? start.offset : 0;
|
|
3977
|
+
if (begin > 0 && begin < text.length && text.charCodeAt(begin - 1) >= 55296 && text.charCodeAt(begin - 1) <= 56319 && text.charCodeAt(begin) >= 56320 && text.charCodeAt(begin) <= 57343) {
|
|
3978
|
+
throw new Error("Invalid command output cursor: offset splits a Unicode code point");
|
|
3979
|
+
}
|
|
3980
|
+
if (begin > text.length) gaps.push("cursor_output_no_longer_retained");
|
|
3981
|
+
let end = Math.min(begin, text.length);
|
|
3982
|
+
for (const character of text.slice(end)) {
|
|
3983
|
+
const bytes = Buffer.byteLength(character, "utf8");
|
|
3984
|
+
if (bytes > remaining) break;
|
|
3985
|
+
remaining -= bytes;
|
|
3986
|
+
end += character.length;
|
|
3987
|
+
}
|
|
3988
|
+
if (end > begin)
|
|
3989
|
+
chunks.push({
|
|
3990
|
+
sequence: row.sequence,
|
|
3991
|
+
stream: payload.stream === "stderr" ? "stderr" : "stdout",
|
|
3992
|
+
streamFidelity: payload.streamFidelity === "merged" ? "merged" : payload.streamFidelity === "separate" ? "separate" : "unknown",
|
|
3993
|
+
chunk: text.slice(begin, end)
|
|
3994
|
+
});
|
|
3995
|
+
if (end < text.length) {
|
|
3996
|
+
sequence = row.sequence;
|
|
3997
|
+
offset = end;
|
|
3998
|
+
break;
|
|
3999
|
+
}
|
|
4000
|
+
sequence = row.sequence + 1;
|
|
4001
|
+
offset = 0;
|
|
4002
|
+
consumed++;
|
|
4003
|
+
}
|
|
4004
|
+
return {
|
|
4005
|
+
chunks,
|
|
4006
|
+
nextCursor: `${input.commandId}:${sequence}:${offset}`,
|
|
4007
|
+
hasMore: consumed < input.rows.length,
|
|
4008
|
+
retention: {
|
|
4009
|
+
source: "retained_session_events",
|
|
4010
|
+
completeness: "unknown",
|
|
4011
|
+
gaps: [...new Set(gaps)]
|
|
4012
|
+
}
|
|
4013
|
+
};
|
|
4014
|
+
}
|
|
4015
|
+
async function readSessionBackgroundCommandOutput(db, input) {
|
|
4016
|
+
const cursor = parseCommandOutputCursor(input.cursor, input.commandId);
|
|
4017
|
+
projectCommandOutputPage({ ...input, rows: [] });
|
|
4018
|
+
const command = await getSessionBackgroundCommand(db, input);
|
|
4019
|
+
if (!command) throw new Error("Background command not found in this session");
|
|
4020
|
+
const rows = await withRlsContext(
|
|
4021
|
+
db,
|
|
4022
|
+
input,
|
|
4023
|
+
async (tx) => await tx.select({
|
|
4024
|
+
sequence: sessionEvents.sequence,
|
|
4025
|
+
// Bound legacy payloads in SQL too: LIMIT alone cannot bound one old event.
|
|
4026
|
+
// Oversized historical rows carry explicit loss rather than allocating an
|
|
4027
|
+
// unbounded JSON value in the API process.
|
|
4028
|
+
payload: sql4`case when octet_length(${sessionEvents.payload}::text) <= 262144
|
|
4029
|
+
then ${sessionEvents.payload}
|
|
4030
|
+
else jsonb_build_object('commandReadProjectionGap', true) end`,
|
|
4031
|
+
payloadCodecVersion: sessionEvents.payloadCodecVersion
|
|
4032
|
+
}).from(sessionEvents).where(
|
|
4033
|
+
and4(
|
|
4034
|
+
eq4(sessionEvents.workspaceId, input.workspaceId),
|
|
4035
|
+
eq4(sessionEvents.sessionId, input.sessionId),
|
|
4036
|
+
eq4(sessionEvents.type, "sandbox.command.output.delta"),
|
|
4037
|
+
sql4`${sessionEvents.payload} ->> 'commandId' = ${input.commandId}`,
|
|
4038
|
+
gte(sessionEvents.sequence, cursor.sequence)
|
|
4039
|
+
)
|
|
4040
|
+
).orderBy(asc2(sessionEvents.sequence)).limit(COMMAND_OUTPUT_PAGE_ROWS + 1)
|
|
4041
|
+
);
|
|
4042
|
+
const page = projectCommandOutputPage({ ...input, rows });
|
|
4043
|
+
const terminal = command.state === "exited" || command.state === "lost";
|
|
4044
|
+
const observed = terminal ? await observeSessionBackgroundCommandCompletion(db, input) : command;
|
|
4045
|
+
return {
|
|
4046
|
+
commandId: command.id,
|
|
4047
|
+
state: command.state,
|
|
4048
|
+
exitCode: command.exitCode,
|
|
4049
|
+
settlementReason: command.settlementReason,
|
|
4050
|
+
...command.failure ? { failure: command.failure } : {},
|
|
4051
|
+
terminal,
|
|
4052
|
+
completionObservedAt: observed?.completionObservedAt ?? null,
|
|
4053
|
+
...page
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
export {
|
|
4058
|
+
TOOL_RESULT_TYPE_BY_CALL_TYPE,
|
|
4059
|
+
historyCallId,
|
|
4060
|
+
historyItemType,
|
|
4061
|
+
interruptedToolCallResult,
|
|
4062
|
+
closePendingSessionToolCallsInTransaction,
|
|
4063
|
+
configureChildLifecycleNotices,
|
|
4064
|
+
childLifecycleNoticesEnabled,
|
|
4065
|
+
boundedChildNoticeText,
|
|
4066
|
+
CHILD_LIFECYCLE_DEDUPE_PREFIX,
|
|
4067
|
+
childRequiresActionDedupeKey,
|
|
4068
|
+
childRequiresActionResolvedDedupeKey,
|
|
4069
|
+
childPausedDedupeKey,
|
|
4070
|
+
childWaitingCapacityDedupeKey,
|
|
4071
|
+
childProgressDedupeKey,
|
|
4072
|
+
buildChildRequiresActionPayload,
|
|
4073
|
+
childRequiresActionSummary,
|
|
4074
|
+
childRequiresActionResolvedSummary,
|
|
4075
|
+
childPausedSummary,
|
|
4076
|
+
childWaitingCapacitySummary,
|
|
4077
|
+
childProgressSummary,
|
|
4078
|
+
boundedChildPausedReason,
|
|
4079
|
+
boundedChildProgressNote,
|
|
4080
|
+
mirrorSessionRealtimeContextInTransaction,
|
|
4081
|
+
renderRealtimeHumanInputContext,
|
|
4082
|
+
renderRealtimeHumanInputRequestContext,
|
|
4083
|
+
renderRealtimeHumanInputResponseContext,
|
|
4084
|
+
SESSION_BACKGROUND_COMMAND_REASON_MAX_BYTES,
|
|
4085
|
+
boundedSessionBackgroundCommandReason,
|
|
4086
|
+
backgroundCommandActivityForSessions,
|
|
4087
|
+
listSessionBackgroundCommands,
|
|
4088
|
+
insertManagedSessionBackgroundCommandInTransaction,
|
|
4089
|
+
insertConnectedMachineSessionBackgroundCommandInTransaction,
|
|
4090
|
+
claimConnectedMachineSessionBackgroundCommands,
|
|
4091
|
+
recordConnectedMachineBackgroundCommandProof,
|
|
4092
|
+
deferConnectedMachineBackgroundCommandReconciliation,
|
|
4093
|
+
settleClaimedConnectedMachineBackgroundCommandWithMutation,
|
|
4094
|
+
requestSessionBackgroundCommandCancellation,
|
|
4095
|
+
requestSessionBackgroundCommandCancellationsInTransaction,
|
|
4096
|
+
requestWorkspaceBackgroundCommandCancellationsInTransaction,
|
|
4097
|
+
settleSessionBackgroundCommandForRetainedProcessInTransaction,
|
|
4098
|
+
settleConnectedMachineSessionBackgroundCommandWithMutation,
|
|
4099
|
+
getSessionBackgroundCommand,
|
|
4100
|
+
observeSessionBackgroundCommandCompletion,
|
|
4101
|
+
COMMAND_OUTPUT_DEFAULT_BYTES,
|
|
4102
|
+
COMMAND_OUTPUT_MAX_BYTES,
|
|
4103
|
+
COMMAND_OUTPUT_PAGE_ROWS,
|
|
4104
|
+
parseCommandOutputCursor,
|
|
4105
|
+
projectCommandOutputPage,
|
|
4106
|
+
readSessionBackgroundCommandOutput,
|
|
4107
|
+
SESSION_ANCESTRY_LIMIT,
|
|
4108
|
+
SESSION_DISCOVERY_CONTROL_TITLE_MAX_CHARS,
|
|
4109
|
+
SESSION_DISCOVERY_CONTROL_TARGET_LIMIT,
|
|
4110
|
+
serializeEffectiveSessionControl,
|
|
4111
|
+
SessionControlInvariantError,
|
|
4112
|
+
SessionControlConflictError,
|
|
4113
|
+
SessionCommandIdempotencyError,
|
|
4114
|
+
AgentCommandAuthorityError,
|
|
4115
|
+
assertAgentCommandAuthorityInTransaction,
|
|
4116
|
+
canonicalSessionCommandHash,
|
|
4117
|
+
WORKSPACE_CONTROL_REQUEST_LOCK_TIMEOUT_MS,
|
|
4118
|
+
configureWorkspaceControlRequestLockTimeoutMs,
|
|
4119
|
+
workspaceControlRequestLockTimeoutMs,
|
|
4120
|
+
WorkspaceControlBusyError,
|
|
4121
|
+
workspaceControlLockBudget,
|
|
4122
|
+
lockWorkspaceInferenceControl,
|
|
4123
|
+
lockWorkspaceInferenceControlForAdmission,
|
|
4124
|
+
lockSessionEventWriteRows,
|
|
4125
|
+
registerSessionTurnAttemptClaim,
|
|
4126
|
+
assertSessionAuthoritySnapshot,
|
|
4127
|
+
sessionAuthoritySnapshotMatchesSession,
|
|
4128
|
+
sessionAuthoritySnapshotsEqual,
|
|
4129
|
+
closeSessionTurnAttemptInTransaction,
|
|
4130
|
+
evaluateSessionControls,
|
|
4131
|
+
evaluateSessionWriteAdmissionControl,
|
|
4132
|
+
evaluateSessionDiscoveryControls,
|
|
4133
|
+
evaluateSessionControl,
|
|
4134
|
+
reserveSessionPromptCommandReceipt,
|
|
4135
|
+
lockSessionPromptCommandOperation,
|
|
4136
|
+
getCompletedSessionPromptCommandReceipt,
|
|
4137
|
+
reserveSessionCommandReceipt,
|
|
4138
|
+
registerSessionWorkflowWakeInTransaction,
|
|
4139
|
+
registerInternalUpdateWakeInTransaction,
|
|
4140
|
+
updateSessionCommandReceiptResult,
|
|
4141
|
+
mutateSessionControlInTransaction,
|
|
4142
|
+
autoResumeSessionBranchInTransaction,
|
|
4143
|
+
mutateWorkspaceControlInTransaction
|
|
4144
|
+
};
|
|
4145
|
+
//# sourceMappingURL=chunk-DUB3LUVM.js.map
|