@opengeni/db 0.22.2 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-CYGFLLMN.js → chunk-7WTDI7Y3.js} +764 -283
- package/dist/chunk-7WTDI7Y3.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-KW6U54V2.js} +26 -2
- package/dist/chunk-KW6U54V2.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +107 -5
- package/dist/index.js +7857 -3898
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1659 -77
- package/dist/schema.js +13 -1
- package/dist/session-control.d.ts +7 -1
- package/dist/session-queue-commands.d.ts +8 -0
- package/dist/session-realtime-context.d.ts +56 -0
- package/dist/session-realtime-ledger.d.ts +188 -0
- package/dist/session-realtime-mirror.d.ts +51 -0
- package/dist/session-realtime-state.d.ts +2 -0
- package/dist/session-realtime-terminal.d.ts +40 -0
- package/dist/session-realtime.d.ts +59 -0
- package/dist/workspace-instruction-policies-schema.d.ts +307 -0
- package/dist/workspace-instruction-policies.d.ts +97 -7
- package/drizzle/0156_slack_reaction_trigger.sql +49 -0
- package/drizzle/0157_session_policy_role_snapshots.sql +1146 -0
- package/drizzle/0158_session_realtime_mode.sql +88 -0
- package/drizzle/0159_session_realtime_ledger.sql +198 -0
- package/drizzle/0160_session_realtime_delegation_terminal.sql +38 -0
- package/drizzle/0161_session_realtime_context_projection.sql +82 -0
- package/drizzle/0162_session_realtime_connection_promotion.sql +53 -0
- package/drizzle/0163_session_realtime_delegation_progress.sql +35 -0
- package/drizzle/0164_session_realtime_models.sql +28 -0
- package/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/package.json +4 -4
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +1033 -101
- package/src/preference-registry.ts +114 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +12 -0
- package/src/schema.ts +486 -43
- package/src/session-control.ts +643 -21
- package/src/session-queue-commands.ts +121 -18
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +276 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +659 -0
- package/src/workspace-instruction-policies-schema.ts +71 -0
- package/src/workspace-instruction-policies.ts +568 -25
- package/dist/chunk-BNGEN5QZ.js.map +0 -1
- package/dist/chunk-CYGFLLMN.js.map +0 -1
package/src/session-control.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
boundWorkspaceControlEvent,
|
|
4
|
+
McpPersonalConnectionDelegations,
|
|
4
5
|
workspaceControlUtf8Bytes,
|
|
5
6
|
type SessionMcpApprovalPolicy,
|
|
6
7
|
type TurnInitiatorContext,
|
|
@@ -8,6 +9,11 @@ import {
|
|
|
8
9
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
9
10
|
import type { Database } from "./index";
|
|
10
11
|
import * as schema from "./schema";
|
|
12
|
+
import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
|
|
13
|
+
import {
|
|
14
|
+
mirrorSessionRealtimeContextInTransaction,
|
|
15
|
+
renderRealtimeHumanInputResponseContext,
|
|
16
|
+
} from "./session-realtime-mirror";
|
|
11
17
|
|
|
12
18
|
export const SESSION_ANCESTRY_LIMIT = 10_000;
|
|
13
19
|
|
|
@@ -1575,6 +1581,55 @@ async function registerInterruptionWakes(
|
|
|
1575
1581
|
return Number(rows[0]?.wakeCount ?? 0);
|
|
1576
1582
|
}
|
|
1577
1583
|
|
|
1584
|
+
/**
|
|
1585
|
+
* Terminal cancellation must wake every affected workflow, including sessions
|
|
1586
|
+
* parked at an approval, capacity, or other no-active-attempt boundary. Those
|
|
1587
|
+
* sessions have no new interruption row, so the ordinary Pause wake selector
|
|
1588
|
+
* cannot discover them. Signal-with-start is intentional here: a closed
|
|
1589
|
+
* workflow cheaply re-reads the terminal row and exits, while an open workflow
|
|
1590
|
+
* is released from an otherwise unbounded durable wait.
|
|
1591
|
+
*/
|
|
1592
|
+
async function registerCancellationWakes(
|
|
1593
|
+
db: Database,
|
|
1594
|
+
input: { workspaceId: string; sessionIds: string[] },
|
|
1595
|
+
): Promise<number> {
|
|
1596
|
+
if (input.sessionIds.length === 0) return 0;
|
|
1597
|
+
const sessionIds = sql.join(
|
|
1598
|
+
input.sessionIds.map((id) => sql`${id}::uuid`),
|
|
1599
|
+
sql`, `,
|
|
1600
|
+
);
|
|
1601
|
+
const rows = await db.execute<{ wakeCount: number | string }>(sql`
|
|
1602
|
+
with eligible as (
|
|
1603
|
+
select
|
|
1604
|
+
session.id as session_id,
|
|
1605
|
+
session.account_id,
|
|
1606
|
+
session.workspace_id,
|
|
1607
|
+
coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
|
|
1608
|
+
as temporal_workflow_id
|
|
1609
|
+
from ${schema.sessions} session
|
|
1610
|
+
where session.workspace_id = ${input.workspaceId}
|
|
1611
|
+
and session.id in (${sessionIds})
|
|
1612
|
+
), upserted as (
|
|
1613
|
+
insert into ${schema.sessionWorkflowWakeOutbox} (
|
|
1614
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1615
|
+
)
|
|
1616
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, 'session_cancelled'
|
|
1617
|
+
from eligible
|
|
1618
|
+
on conflict (session_id) do update set
|
|
1619
|
+
wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1620
|
+
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1621
|
+
reason = excluded.reason,
|
|
1622
|
+
attempts = 0,
|
|
1623
|
+
next_attempt_at = now(),
|
|
1624
|
+
last_error = null,
|
|
1625
|
+
updated_at = now()
|
|
1626
|
+
returning session_id
|
|
1627
|
+
)
|
|
1628
|
+
select count(*)::bigint as "wakeCount" from upserted
|
|
1629
|
+
`);
|
|
1630
|
+
return Number(rows[0]?.wakeCount ?? 0);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1578
1633
|
/** Register one exact post-commit Temporal nudge without encoding eligibility in
|
|
1579
1634
|
* the transport. The workflow re-reads canonical Postgres state; coalescing is
|
|
1580
1635
|
* revisioned so a lost or stale delivery cannot hide a later mutation. */
|
|
@@ -1786,9 +1841,495 @@ export type SessionControlMutationResult = {
|
|
|
1786
1841
|
workspaceControlEventId: string;
|
|
1787
1842
|
interruptionCount: number;
|
|
1788
1843
|
wakeCount: number;
|
|
1844
|
+
cancelledSessionCount: number;
|
|
1845
|
+
cancelledTurnCount: number;
|
|
1846
|
+
affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }>;
|
|
1789
1847
|
replay: boolean;
|
|
1790
1848
|
};
|
|
1791
1849
|
|
|
1850
|
+
async function loadSessionSubtreeIds(
|
|
1851
|
+
db: Database,
|
|
1852
|
+
workspaceId: string,
|
|
1853
|
+
rootSessionId: string,
|
|
1854
|
+
): Promise<{ sessionIds: string[]; rootParentSessionId: string | null }> {
|
|
1855
|
+
const rows: Array<{
|
|
1856
|
+
id: string;
|
|
1857
|
+
rootParentSessionId: string | null;
|
|
1858
|
+
depth: number | string;
|
|
1859
|
+
cycle: boolean;
|
|
1860
|
+
}> = await db.execute(sql`
|
|
1861
|
+
with recursive subtree(id, root_parent_session_id, depth, path, cycle) as (
|
|
1862
|
+
select session.id, session.parent_session_id, 0::integer, array[session.id]::uuid[], false
|
|
1863
|
+
from ${schema.sessions} session
|
|
1864
|
+
where session.workspace_id = ${workspaceId} and session.id = ${rootSessionId}
|
|
1865
|
+
union all
|
|
1866
|
+
select child.id, parent.root_parent_session_id, parent.depth + 1,
|
|
1867
|
+
parent.path || child.id, child.id = any(parent.path)
|
|
1868
|
+
from subtree parent
|
|
1869
|
+
join ${schema.sessions} child
|
|
1870
|
+
on child.workspace_id = ${workspaceId} and child.parent_session_id = parent.id
|
|
1871
|
+
where parent.depth < ${SESSION_ANCESTRY_LIMIT} and not parent.cycle
|
|
1872
|
+
)
|
|
1873
|
+
select id, root_parent_session_id as "rootParentSessionId", depth, cycle
|
|
1874
|
+
from subtree
|
|
1875
|
+
order by id
|
|
1876
|
+
`);
|
|
1877
|
+
if (rows.length === 0) {
|
|
1878
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} does not exist`);
|
|
1879
|
+
}
|
|
1880
|
+
if (rows.some((row) => row.cycle || Number(row.depth) >= SESSION_ANCESTRY_LIMIT)) {
|
|
1881
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} subtree is invalid`);
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
sessionIds: rows.map((row) => row.id),
|
|
1885
|
+
rootParentSessionId: rows[0]?.rootParentSessionId ?? null,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const TERMINAL_CANCELLATION_LIVE_TURN_STATUSES = [
|
|
1890
|
+
"queued",
|
|
1891
|
+
"running",
|
|
1892
|
+
"requires_action",
|
|
1893
|
+
"recovering",
|
|
1894
|
+
"waiting_capacity",
|
|
1895
|
+
];
|
|
1896
|
+
|
|
1897
|
+
async function loadCancellationTurnIds(
|
|
1898
|
+
db: Database,
|
|
1899
|
+
workspaceId: string,
|
|
1900
|
+
sessionIds: string[],
|
|
1901
|
+
): Promise<string[]> {
|
|
1902
|
+
const rows = await db
|
|
1903
|
+
.select({ id: schema.sessionTurns.id })
|
|
1904
|
+
.from(schema.sessionTurns)
|
|
1905
|
+
.where(
|
|
1906
|
+
and(
|
|
1907
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
1908
|
+
inArray(schema.sessionTurns.sessionId, sessionIds),
|
|
1909
|
+
inArray(schema.sessionTurns.status, TERMINAL_CANCELLATION_LIVE_TURN_STATUSES),
|
|
1910
|
+
),
|
|
1911
|
+
)
|
|
1912
|
+
.orderBy(schema.sessionTurns.id);
|
|
1913
|
+
return rows.map((row) => row.id);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
async function enqueueCancelledChildOutboxInTransaction(
|
|
1917
|
+
db: Database,
|
|
1918
|
+
input: {
|
|
1919
|
+
workspaceId: string;
|
|
1920
|
+
rootSession: typeof schema.sessions.$inferSelect;
|
|
1921
|
+
parentSession: typeof schema.sessions.$inferSelect | null;
|
|
1922
|
+
},
|
|
1923
|
+
): Promise<void> {
|
|
1924
|
+
const parentSessionId = input.rootSession.parentSessionId;
|
|
1925
|
+
if (!parentSessionId) return;
|
|
1926
|
+
if (!input.parentSession || input.parentSession.id !== parentSessionId) {
|
|
1927
|
+
throw new SessionControlInvariantError(
|
|
1928
|
+
`Parent session ${parentSessionId} was not locked with cancellation root ${input.rootSession.id}`,
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
if (input.parentSession.status === "cancelled") return;
|
|
1932
|
+
let personalConnectionDelegations: (typeof schema.sessionTurns.$inferSelect)["personalConnectionDelegations"] =
|
|
1933
|
+
[];
|
|
1934
|
+
if (input.rootSession.parentTurnId) {
|
|
1935
|
+
const [parentTurn] = await db
|
|
1936
|
+
.select({ delegations: schema.sessionTurns.personalConnectionDelegations })
|
|
1937
|
+
.from(schema.sessionTurns)
|
|
1938
|
+
.where(
|
|
1939
|
+
and(
|
|
1940
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
1941
|
+
eq(schema.sessionTurns.sessionId, parentSessionId),
|
|
1942
|
+
eq(schema.sessionTurns.id, input.rootSession.parentTurnId),
|
|
1943
|
+
),
|
|
1944
|
+
)
|
|
1945
|
+
.limit(1);
|
|
1946
|
+
if (parentTurn) {
|
|
1947
|
+
const parsed = McpPersonalConnectionDelegations.safeParse(parentTurn.delegations);
|
|
1948
|
+
if (!parsed.success) {
|
|
1949
|
+
throw new SessionControlInvariantError(
|
|
1950
|
+
`Invalid personal MCP delegation snapshot at session_turns:${input.workspaceId}:${parentSessionId}:${input.rootSession.parentTurnId}`,
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
personalConnectionDelegations = parsed.data.map((delegation) => ({ ...delegation }));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
await db
|
|
1957
|
+
.insert(schema.sessionSystemUpdateOutbox)
|
|
1958
|
+
.values({
|
|
1959
|
+
accountId: input.rootSession.accountId,
|
|
1960
|
+
workspaceId: input.workspaceId,
|
|
1961
|
+
sourceSessionId: input.rootSession.id,
|
|
1962
|
+
targetSessionId: parentSessionId,
|
|
1963
|
+
dedupeKey: `child-completion:${input.rootSession.id}:cancelled`,
|
|
1964
|
+
kind: "child_terminal_result",
|
|
1965
|
+
classification: "info",
|
|
1966
|
+
sourceId: input.rootSession.id,
|
|
1967
|
+
summary: "Child session was terminally cancelled.",
|
|
1968
|
+
payload: {
|
|
1969
|
+
type: "child_terminal_result",
|
|
1970
|
+
childSessionId: input.rootSession.id,
|
|
1971
|
+
status: "cancelled",
|
|
1972
|
+
},
|
|
1973
|
+
lineage: {
|
|
1974
|
+
childSessionId: input.rootSession.id,
|
|
1975
|
+
parentSessionId,
|
|
1976
|
+
...(input.rootSession.parentTurnId ? { parentTurnId: input.rootSession.parentTurnId } : {}),
|
|
1977
|
+
},
|
|
1978
|
+
personalConnectionDelegations,
|
|
1979
|
+
})
|
|
1980
|
+
.onConflictDoNothing({
|
|
1981
|
+
target: [
|
|
1982
|
+
schema.sessionSystemUpdateOutbox.workspaceId,
|
|
1983
|
+
schema.sessionSystemUpdateOutbox.dedupeKey,
|
|
1984
|
+
],
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function assertSessionBranchIsNotCancelled(
|
|
1989
|
+
db: Database,
|
|
1990
|
+
workspaceId: string,
|
|
1991
|
+
sessionId: string,
|
|
1992
|
+
): Promise<void> {
|
|
1993
|
+
const rows = await db.execute<{
|
|
1994
|
+
cancelled: boolean;
|
|
1995
|
+
invalid: boolean;
|
|
1996
|
+
}>(sql`
|
|
1997
|
+
with recursive ancestry(id, parent_id, status, depth, path, cycle) as (
|
|
1998
|
+
select session.id, session.parent_session_id, session.status, 0::integer,
|
|
1999
|
+
array[session.id]::uuid[], false
|
|
2000
|
+
from ${schema.sessions} session
|
|
2001
|
+
where session.workspace_id = ${workspaceId} and session.id = ${sessionId}
|
|
2002
|
+
union all
|
|
2003
|
+
select parent.id, parent.parent_session_id, parent.status, child.depth + 1,
|
|
2004
|
+
child.path || parent.id, parent.id = any(child.path)
|
|
2005
|
+
from ancestry child
|
|
2006
|
+
join ${schema.sessions} parent
|
|
2007
|
+
on parent.workspace_id = ${workspaceId} and parent.id = child.parent_id
|
|
2008
|
+
where child.depth < ${SESSION_ANCESTRY_LIMIT} and not child.cycle
|
|
2009
|
+
)
|
|
2010
|
+
select
|
|
2011
|
+
coalesce(bool_or(status = 'cancelled'), false) as cancelled,
|
|
2012
|
+
count(*) = 0 or coalesce(bool_or(cycle), false)
|
|
2013
|
+
or coalesce(max(depth), 0) >= ${SESSION_ANCESTRY_LIMIT} as invalid
|
|
2014
|
+
from ancestry
|
|
2015
|
+
`);
|
|
2016
|
+
if (rows[0]?.invalid) {
|
|
2017
|
+
throw new SessionControlInvariantError(`Session ${sessionId} ancestry is invalid`);
|
|
2018
|
+
}
|
|
2019
|
+
if (rows[0]?.cancelled) {
|
|
2020
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot accept work");
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
async function cancelSessionSubtreeInTransaction(
|
|
2025
|
+
db: Database,
|
|
2026
|
+
input: {
|
|
2027
|
+
accountId: string;
|
|
2028
|
+
workspaceId: string;
|
|
2029
|
+
rootSessionId: string;
|
|
2030
|
+
sessionIds: string[];
|
|
2031
|
+
lockedSessions: Array<typeof schema.sessions.$inferSelect>;
|
|
2032
|
+
candidateTurnIds: string[];
|
|
2033
|
+
lockedTurns: Array<typeof schema.sessionTurns.$inferSelect>;
|
|
2034
|
+
actor: string;
|
|
2035
|
+
reason: string | null;
|
|
2036
|
+
operationId: string;
|
|
2037
|
+
rootControlEventId: string;
|
|
2038
|
+
},
|
|
2039
|
+
): Promise<{
|
|
2040
|
+
cancelledSessionCount: number;
|
|
2041
|
+
cancelledTurnCount: number;
|
|
2042
|
+
affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }>;
|
|
2043
|
+
}> {
|
|
2044
|
+
const candidateTurnIdSet = new Set(input.candidateTurnIds);
|
|
2045
|
+
const candidateTurns = input.lockedTurns.filter((turn) => candidateTurnIdSet.has(turn.id));
|
|
2046
|
+
if (candidateTurns.length !== candidateTurnIdSet.size) {
|
|
2047
|
+
throw new SessionControlInvariantError("Cancellation candidate turns changed while locking");
|
|
2048
|
+
}
|
|
2049
|
+
// The outer command acquired every session and candidate turn in one UUID-
|
|
2050
|
+
// sorted call before the actor attempt. Repeat only those already-held locks
|
|
2051
|
+
// so this event-writing suffix retains a directly auditable lock contract;
|
|
2052
|
+
// this call must never discover or acquire a new row.
|
|
2053
|
+
const suffixLocks = await lockSessionEventWriteRows(db, {
|
|
2054
|
+
workspaceId: input.workspaceId,
|
|
2055
|
+
controlLock: "already_locked",
|
|
2056
|
+
workspaceLock: "already_locked",
|
|
2057
|
+
sessionIds: input.lockedSessions.map((session) => session.id),
|
|
2058
|
+
turnIds: input.candidateTurnIds,
|
|
2059
|
+
});
|
|
2060
|
+
if (
|
|
2061
|
+
suffixLocks.sessions.length !== input.lockedSessions.length ||
|
|
2062
|
+
suffixLocks.turns.length !== candidateTurnIdSet.size
|
|
2063
|
+
) {
|
|
2064
|
+
throw new SessionControlInvariantError("Cancellation rows changed under canonical locks");
|
|
2065
|
+
}
|
|
2066
|
+
const sessionIdSet = new Set(input.sessionIds);
|
|
2067
|
+
const rootSession = input.lockedSessions.find((session) => session.id === input.rootSessionId);
|
|
2068
|
+
if (!rootSession) {
|
|
2069
|
+
throw new SessionControlInvariantError(
|
|
2070
|
+
`Cancellation root ${input.rootSessionId} was not locked with its subtree`,
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
const rootParentSession = rootSession.parentSessionId
|
|
2074
|
+
? (input.lockedSessions.find((session) => session.id === rootSession.parentSessionId) ?? null)
|
|
2075
|
+
: null;
|
|
2076
|
+
const candidateTurnById = new Map(candidateTurns.map((turn) => [turn.id, turn]));
|
|
2077
|
+
const liveTurnIds = new Set(
|
|
2078
|
+
input.lockedSessions
|
|
2079
|
+
.filter((session) => session.activeTurnId !== null)
|
|
2080
|
+
.flatMap((session) => {
|
|
2081
|
+
const turn = session.activeTurnId ? candidateTurnById.get(session.activeTurnId) : undefined;
|
|
2082
|
+
if (turn?.activeAttemptId === null) return [];
|
|
2083
|
+
return turn ? [turn.id] : [];
|
|
2084
|
+
}),
|
|
2085
|
+
);
|
|
2086
|
+
const immediatelyCancelledTurns = candidateTurns.filter((turn) => !liveTurnIds.has(turn.id));
|
|
2087
|
+
const cancelledTurnsBySession = new Map<string, typeof immediatelyCancelledTurns>();
|
|
2088
|
+
for (const turn of immediatelyCancelledTurns) {
|
|
2089
|
+
const turns = cancelledTurnsBySession.get(turn.sessionId);
|
|
2090
|
+
if (turns) turns.push(turn);
|
|
2091
|
+
else cancelledTurnsBySession.set(turn.sessionId, [turn]);
|
|
2092
|
+
}
|
|
2093
|
+
const immediatelyCancelledTurnIds = immediatelyCancelledTurns.map((turn) => turn.id);
|
|
2094
|
+
const now = new Date();
|
|
2095
|
+
let cancelledHumanInputs: Array<{
|
|
2096
|
+
id: string;
|
|
2097
|
+
sessionId: string;
|
|
2098
|
+
turnId: string;
|
|
2099
|
+
questions: (typeof schema.sessionHumanInputRequests.$inferSelect)["questions"];
|
|
2100
|
+
}> = [];
|
|
2101
|
+
if (immediatelyCancelledTurnIds.length > 0) {
|
|
2102
|
+
await db
|
|
2103
|
+
.update(schema.sessionTurns)
|
|
2104
|
+
.set({
|
|
2105
|
+
status: "cancelled",
|
|
2106
|
+
activeAttemptId: null,
|
|
2107
|
+
cancelledBy: input.actor,
|
|
2108
|
+
cancelReason: input.reason ?? "session_cancelled",
|
|
2109
|
+
version: sql`${schema.sessionTurns.version} + 1`,
|
|
2110
|
+
finishedAt: now,
|
|
2111
|
+
updatedAt: now,
|
|
2112
|
+
})
|
|
2113
|
+
.where(inArray(schema.sessionTurns.id, immediatelyCancelledTurnIds));
|
|
2114
|
+
cancelledHumanInputs = await db
|
|
2115
|
+
.update(schema.sessionHumanInputRequests)
|
|
2116
|
+
.set({
|
|
2117
|
+
status: "cancelled",
|
|
2118
|
+
response: { outcome: "cancelled" },
|
|
2119
|
+
respondedBy: input.actor,
|
|
2120
|
+
respondedAt: now,
|
|
2121
|
+
updatedAt: now,
|
|
2122
|
+
})
|
|
2123
|
+
.where(
|
|
2124
|
+
and(
|
|
2125
|
+
eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
|
|
2126
|
+
inArray(schema.sessionHumanInputRequests.turnId, immediatelyCancelledTurnIds),
|
|
2127
|
+
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
2128
|
+
),
|
|
2129
|
+
)
|
|
2130
|
+
.returning({
|
|
2131
|
+
id: schema.sessionHumanInputRequests.id,
|
|
2132
|
+
sessionId: schema.sessionHumanInputRequests.sessionId,
|
|
2133
|
+
turnId: schema.sessionHumanInputRequests.turnId,
|
|
2134
|
+
questions: schema.sessionHumanInputRequests.questions,
|
|
2135
|
+
});
|
|
2136
|
+
await db
|
|
2137
|
+
.update(schema.codexCapacityWaiters)
|
|
2138
|
+
.set({ status: "superseded", lastWakeReason: "session_cancelled", updatedAt: now })
|
|
2139
|
+
.where(
|
|
2140
|
+
and(
|
|
2141
|
+
eq(schema.codexCapacityWaiters.workspaceId, input.workspaceId),
|
|
2142
|
+
inArray(schema.codexCapacityWaiters.blockedTurnId, immediatelyCancelledTurnIds),
|
|
2143
|
+
eq(schema.codexCapacityWaiters.status, "waiting"),
|
|
2144
|
+
),
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
const cancelledSystemUpdates = await db
|
|
2148
|
+
.update(schema.sessionSystemUpdates)
|
|
2149
|
+
.set({ state: "cancelled" })
|
|
2150
|
+
.where(
|
|
2151
|
+
and(
|
|
2152
|
+
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
2153
|
+
inArray(schema.sessionSystemUpdates.sessionId, input.sessionIds),
|
|
2154
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
2155
|
+
),
|
|
2156
|
+
)
|
|
2157
|
+
.returning({
|
|
2158
|
+
id: schema.sessionSystemUpdates.id,
|
|
2159
|
+
sessionId: schema.sessionSystemUpdates.sessionId,
|
|
2160
|
+
});
|
|
2161
|
+
const humanInputsByTurn = new Map<string, typeof cancelledHumanInputs>();
|
|
2162
|
+
for (const humanInput of cancelledHumanInputs) {
|
|
2163
|
+
const inputs = humanInputsByTurn.get(humanInput.turnId);
|
|
2164
|
+
if (inputs) inputs.push(humanInput);
|
|
2165
|
+
else humanInputsByTurn.set(humanInput.turnId, [humanInput]);
|
|
2166
|
+
}
|
|
2167
|
+
const systemUpdatesBySession = new Map<string, typeof cancelledSystemUpdates>();
|
|
2168
|
+
for (const update of cancelledSystemUpdates) {
|
|
2169
|
+
const updates = systemUpdatesBySession.get(update.sessionId);
|
|
2170
|
+
if (updates) updates.push(update);
|
|
2171
|
+
else systemUpdatesBySession.set(update.sessionId, [update]);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }> = [];
|
|
2175
|
+
for (const session of input.lockedSessions) {
|
|
2176
|
+
if (!sessionIdSet.has(session.id)) continue;
|
|
2177
|
+
let sequence = session.lastSequence + (session.id === input.rootSessionId ? 1 : 0);
|
|
2178
|
+
const cancelledTurns = cancelledTurnsBySession.get(session.id) ?? [];
|
|
2179
|
+
const preinsertedEvents: Array<{ id: string; sequence: number }> = [];
|
|
2180
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [];
|
|
2181
|
+
for (const update of (systemUpdatesBySession.get(session.id) ?? []).sort((left, right) =>
|
|
2182
|
+
left.id.localeCompare(right.id),
|
|
2183
|
+
)) {
|
|
2184
|
+
eventValues.push({
|
|
2185
|
+
accountId: input.accountId,
|
|
2186
|
+
workspaceId: input.workspaceId,
|
|
2187
|
+
sessionId: session.id,
|
|
2188
|
+
sequence: ++sequence,
|
|
2189
|
+
type: "system.update.cancelled",
|
|
2190
|
+
payload: { updateId: update.id, reason: "session_cancelled" },
|
|
2191
|
+
occurredAt: now,
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
for (const turn of cancelledTurns) {
|
|
2195
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(db, {
|
|
2196
|
+
accountId: input.accountId,
|
|
2197
|
+
workspaceId: input.workspaceId,
|
|
2198
|
+
sessionId: session.id,
|
|
2199
|
+
turnId: turn.id,
|
|
2200
|
+
reason: "session_cancelled",
|
|
2201
|
+
sequence,
|
|
2202
|
+
now,
|
|
2203
|
+
});
|
|
2204
|
+
sequence = closedTools.sequence;
|
|
2205
|
+
preinsertedEvents.push(
|
|
2206
|
+
...closedTools.events.map((event) => ({ id: event.id, sequence: event.sequence })),
|
|
2207
|
+
);
|
|
2208
|
+
for (const humanInput of (humanInputsByTurn.get(turn.id) ?? []).sort((left, right) =>
|
|
2209
|
+
left.id.localeCompare(right.id),
|
|
2210
|
+
)) {
|
|
2211
|
+
eventValues.push({
|
|
2212
|
+
accountId: input.accountId,
|
|
2213
|
+
workspaceId: input.workspaceId,
|
|
2214
|
+
sessionId: session.id,
|
|
2215
|
+
sequence: ++sequence,
|
|
2216
|
+
type: "user.humanInputResponse",
|
|
2217
|
+
turnId: turn.id,
|
|
2218
|
+
turnGeneration: turn.executionGeneration,
|
|
2219
|
+
...(turn.id === session.activeTurnId ? { turnAssociation: "current" as const } : {}),
|
|
2220
|
+
payload: { requestId: humanInput.id, response: { outcome: "cancelled" } },
|
|
2221
|
+
occurredAt: now,
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
eventValues.push({
|
|
2225
|
+
accountId: input.accountId,
|
|
2226
|
+
workspaceId: input.workspaceId,
|
|
2227
|
+
sessionId: session.id,
|
|
2228
|
+
sequence: ++sequence,
|
|
2229
|
+
type: "turn.cancelled",
|
|
2230
|
+
turnId: turn.id,
|
|
2231
|
+
turnGeneration: turn.executionGeneration,
|
|
2232
|
+
...(turn.id === session.activeTurnId ? { turnAssociation: "current" as const } : {}),
|
|
2233
|
+
payload: {
|
|
2234
|
+
reason: input.reason ?? "session_cancelled",
|
|
2235
|
+
operationId: input.operationId,
|
|
2236
|
+
},
|
|
2237
|
+
occurredAt: now,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
if (session.status !== "cancelled") {
|
|
2241
|
+
eventValues.push({
|
|
2242
|
+
accountId: input.accountId,
|
|
2243
|
+
workspaceId: input.workspaceId,
|
|
2244
|
+
sessionId: session.id,
|
|
2245
|
+
sequence: ++sequence,
|
|
2246
|
+
type: "session.status.changed",
|
|
2247
|
+
payload: {
|
|
2248
|
+
status: "cancelled",
|
|
2249
|
+
reason: input.reason ?? "session_cancelled",
|
|
2250
|
+
operationId: input.operationId,
|
|
2251
|
+
},
|
|
2252
|
+
occurredAt: now,
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
const inserted =
|
|
2256
|
+
eventValues.length > 0
|
|
2257
|
+
? await db.insert(schema.sessionEvents).values(eventValues).returning({
|
|
2258
|
+
id: schema.sessionEvents.id,
|
|
2259
|
+
sequence: schema.sessionEvents.sequence,
|
|
2260
|
+
type: schema.sessionEvents.type,
|
|
2261
|
+
turnId: schema.sessionEvents.turnId,
|
|
2262
|
+
payload: schema.sessionEvents.payload,
|
|
2263
|
+
})
|
|
2264
|
+
: [];
|
|
2265
|
+
const cancelledHumanInputsById = new Map(
|
|
2266
|
+
cancelledHumanInputs
|
|
2267
|
+
.filter((request) => request.sessionId === session.id)
|
|
2268
|
+
.map((request) => [request.id, request]),
|
|
2269
|
+
);
|
|
2270
|
+
for (const event of inserted) {
|
|
2271
|
+
if (event.type !== "user.humanInputResponse") continue;
|
|
2272
|
+
const payload = event.payload as { requestId?: unknown };
|
|
2273
|
+
const request =
|
|
2274
|
+
typeof payload.requestId === "string"
|
|
2275
|
+
? cancelledHumanInputsById.get(payload.requestId)
|
|
2276
|
+
: null;
|
|
2277
|
+
if (!request) continue;
|
|
2278
|
+
await mirrorSessionRealtimeContextInTransaction(db, {
|
|
2279
|
+
accountId: input.accountId,
|
|
2280
|
+
workspaceId: input.workspaceId,
|
|
2281
|
+
sessionId: session.id,
|
|
2282
|
+
sourceKind: "human_input_response",
|
|
2283
|
+
sourceId: event.id,
|
|
2284
|
+
turnId: event.turnId,
|
|
2285
|
+
channel: null,
|
|
2286
|
+
text: renderRealtimeHumanInputResponseContext({
|
|
2287
|
+
requestId: request.id,
|
|
2288
|
+
questions: request.questions,
|
|
2289
|
+
response: { outcome: "cancelled" },
|
|
2290
|
+
}),
|
|
2291
|
+
payload: {
|
|
2292
|
+
requestId: request.id,
|
|
2293
|
+
outcome: "cancelled",
|
|
2294
|
+
sourceEventId: event.id,
|
|
2295
|
+
},
|
|
2296
|
+
now,
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
const liveTurnId =
|
|
2300
|
+
session.activeTurnId && liveTurnIds.has(session.activeTurnId) ? session.activeTurnId : null;
|
|
2301
|
+
await db
|
|
2302
|
+
.update(schema.sessions)
|
|
2303
|
+
.set({
|
|
2304
|
+
status: "cancelled",
|
|
2305
|
+
activeTurnId: liveTurnId,
|
|
2306
|
+
queueVersion: sql`${schema.sessions.queueVersion} + 1`,
|
|
2307
|
+
queueHeadPosition: 0,
|
|
2308
|
+
queueTailPosition: 0,
|
|
2309
|
+
lastSequence: sequence,
|
|
2310
|
+
updatedAt: now,
|
|
2311
|
+
})
|
|
2312
|
+
.where(
|
|
2313
|
+
and(eq(schema.sessions.workspaceId, input.workspaceId), eq(schema.sessions.id, session.id)),
|
|
2314
|
+
);
|
|
2315
|
+
const eventIds = [...preinsertedEvents, ...inserted]
|
|
2316
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
2317
|
+
.map((event) => event.id);
|
|
2318
|
+
if (session.id === input.rootSessionId) eventIds.unshift(input.rootControlEventId);
|
|
2319
|
+
if (eventIds.length > 0) affectedSessionEvents.push({ sessionId: session.id, eventIds });
|
|
2320
|
+
}
|
|
2321
|
+
await enqueueCancelledChildOutboxInTransaction(db, {
|
|
2322
|
+
workspaceId: input.workspaceId,
|
|
2323
|
+
rootSession,
|
|
2324
|
+
parentSession: rootParentSession,
|
|
2325
|
+
});
|
|
2326
|
+
return {
|
|
2327
|
+
cancelledSessionCount: input.sessionIds.length,
|
|
2328
|
+
cancelledTurnCount: candidateTurns.length,
|
|
2329
|
+
affectedSessionEvents,
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
1792
2333
|
export async function mutateSessionControlInTransaction(
|
|
1793
2334
|
db: Database,
|
|
1794
2335
|
input: {
|
|
@@ -1797,22 +2338,53 @@ export async function mutateSessionControlInTransaction(
|
|
|
1797
2338
|
sessionId: string;
|
|
1798
2339
|
actor: SessionCommandActor;
|
|
1799
2340
|
operationKey: string;
|
|
1800
|
-
action: "pause" | "resume";
|
|
2341
|
+
action: "pause" | "resume" | "cancel";
|
|
1801
2342
|
reason?: string | null;
|
|
1802
2343
|
expectedControlEtag?: string | null;
|
|
1803
2344
|
},
|
|
1804
2345
|
): Promise<SessionControlMutationResult> {
|
|
1805
2346
|
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
1806
|
-
|
|
2347
|
+
const cancellationSubtree =
|
|
2348
|
+
input.action === "cancel"
|
|
2349
|
+
? await loadSessionSubtreeIds(db, input.workspaceId, input.sessionId)
|
|
2350
|
+
: { sessionIds: [input.sessionId], rootParentSessionId: null };
|
|
2351
|
+
const cancellationSessionIds = cancellationSubtree.sessionIds;
|
|
2352
|
+
const cancellationTurnIds =
|
|
2353
|
+
input.action === "cancel"
|
|
2354
|
+
? await loadCancellationTurnIds(db, input.workspaceId, cancellationSessionIds)
|
|
2355
|
+
: [];
|
|
2356
|
+
const locks = await lockSessionEventWriteRows(db, {
|
|
1807
2357
|
workspaceId: input.workspaceId,
|
|
1808
2358
|
controlLock: "already_locked",
|
|
1809
2359
|
sessionIds:
|
|
1810
2360
|
input.actor.type === "agent_attempt"
|
|
1811
|
-
? [
|
|
1812
|
-
|
|
1813
|
-
|
|
2361
|
+
? [
|
|
2362
|
+
input.actor.sessionId,
|
|
2363
|
+
...cancellationSessionIds,
|
|
2364
|
+
...(cancellationSubtree.rootParentSessionId
|
|
2365
|
+
? [cancellationSubtree.rootParentSessionId]
|
|
2366
|
+
: []),
|
|
2367
|
+
]
|
|
2368
|
+
: [
|
|
2369
|
+
...cancellationSessionIds,
|
|
2370
|
+
...(cancellationSubtree.rootParentSessionId
|
|
2371
|
+
? [cancellationSubtree.rootParentSessionId]
|
|
2372
|
+
: []),
|
|
2373
|
+
],
|
|
2374
|
+
turnIds:
|
|
2375
|
+
input.actor.type === "agent_attempt"
|
|
2376
|
+
? [input.actor.turnId, ...cancellationTurnIds]
|
|
2377
|
+
: cancellationTurnIds,
|
|
1814
2378
|
attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
|
|
1815
2379
|
});
|
|
2380
|
+
if (input.action === "cancel") {
|
|
2381
|
+
const rootSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2382
|
+
if (!rootSession || rootSession.parentSessionId !== cancellationSubtree.rootParentSessionId) {
|
|
2383
|
+
throw new SessionControlInvariantError(
|
|
2384
|
+
`Session ${input.sessionId} parent changed while establishing cancellation locks`,
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
1816
2388
|
const hash = canonicalSessionCommandHash({
|
|
1817
2389
|
action: input.action,
|
|
1818
2390
|
reason: input.reason ?? null,
|
|
@@ -1848,6 +2420,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
1848
2420
|
workspaceControlEventId,
|
|
1849
2421
|
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
1850
2422
|
wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
|
|
2423
|
+
cancelledSessionCount: Number(reserved.receipt.result.cancelledSessionCount ?? 0),
|
|
2424
|
+
cancelledTurnCount: Number(reserved.receipt.result.cancelledTurnCount ?? 0),
|
|
2425
|
+
affectedSessionEvents: Array.isArray(reserved.receipt.result.affectedSessionEvents)
|
|
2426
|
+
? (reserved.receipt.result.affectedSessionEvents as Array<{
|
|
2427
|
+
sessionId: string;
|
|
2428
|
+
eventIds: string[];
|
|
2429
|
+
}>)
|
|
2430
|
+
: [{ sessionId: input.sessionId, eventIds: [sessionControlEventId] }],
|
|
1851
2431
|
replay: true,
|
|
1852
2432
|
};
|
|
1853
2433
|
}
|
|
@@ -1856,9 +2436,12 @@ export async function mutateSessionControlInTransaction(
|
|
|
1856
2436
|
workspaceId: input.workspaceId,
|
|
1857
2437
|
actor: input.actor,
|
|
1858
2438
|
targetSessionId: input.sessionId,
|
|
1859
|
-
action: input.action,
|
|
2439
|
+
action: input.action === "cancel" ? "pause" : input.action,
|
|
1860
2440
|
});
|
|
1861
2441
|
}
|
|
2442
|
+
if (input.action === "resume") {
|
|
2443
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2444
|
+
}
|
|
1862
2445
|
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
1863
2446
|
workspaceControl: workspace,
|
|
1864
2447
|
});
|
|
@@ -1868,10 +2451,10 @@ export async function mutateSessionControlInTransaction(
|
|
|
1868
2451
|
|
|
1869
2452
|
const revision = nextRevision(workspace);
|
|
1870
2453
|
await advanceWorkspaceRevision(db, input.workspaceId, revision);
|
|
1871
|
-
const
|
|
2454
|
+
const updatedRows = await db
|
|
1872
2455
|
.update(schema.sessions)
|
|
1873
2456
|
.set(
|
|
1874
|
-
input.action === "pause"
|
|
2457
|
+
input.action === "pause" || input.action === "cancel"
|
|
1875
2458
|
? {
|
|
1876
2459
|
directControlState: "paused",
|
|
1877
2460
|
directPauseRevision: revision,
|
|
@@ -1901,13 +2484,16 @@ export async function mutateSessionControlInTransaction(
|
|
|
1901
2484
|
.where(
|
|
1902
2485
|
and(
|
|
1903
2486
|
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
1904
|
-
|
|
2487
|
+
input.action === "cancel"
|
|
2488
|
+
? inArray(schema.sessions.id, cancellationSessionIds)
|
|
2489
|
+
: eq(schema.sessions.id, input.sessionId),
|
|
1905
2490
|
),
|
|
1906
2491
|
)
|
|
1907
2492
|
.returning({
|
|
1908
2493
|
id: schema.sessions.id,
|
|
1909
2494
|
lastSequence: schema.sessions.lastSequence,
|
|
1910
2495
|
});
|
|
2496
|
+
const updated = updatedRows.find((row) => row.id === input.sessionId);
|
|
1911
2497
|
if (!updated) throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
|
|
1912
2498
|
|
|
1913
2499
|
const actor =
|
|
@@ -1920,14 +2506,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
1920
2506
|
revision,
|
|
1921
2507
|
scope: "session",
|
|
1922
2508
|
rootSessionId: input.sessionId,
|
|
1923
|
-
action: input.action,
|
|
2509
|
+
action: input.action === "cancel" ? "pause" : input.action,
|
|
1924
2510
|
automatic: false,
|
|
1925
2511
|
reason: input.reason ?? null,
|
|
1926
2512
|
actor,
|
|
1927
2513
|
});
|
|
1928
2514
|
|
|
1929
2515
|
const interruptionCount =
|
|
1930
|
-
input.action === "pause"
|
|
2516
|
+
input.action === "pause" || input.action === "cancel"
|
|
1931
2517
|
? await interruptDescendantAttempts(db, {
|
|
1932
2518
|
accountId: input.accountId,
|
|
1933
2519
|
workspaceId: input.workspaceId,
|
|
@@ -1938,16 +2524,21 @@ export async function mutateSessionControlInTransaction(
|
|
|
1938
2524
|
})
|
|
1939
2525
|
: 0;
|
|
1940
2526
|
const wakeCount =
|
|
1941
|
-
input.action === "
|
|
1942
|
-
? await
|
|
1943
|
-
operationId: reserved.receipt.id,
|
|
1944
|
-
reason: "session_pause_interruption",
|
|
1945
|
-
})
|
|
1946
|
-
: await registerDescendantWakes(db, {
|
|
2527
|
+
input.action === "cancel"
|
|
2528
|
+
? await registerCancellationWakes(db, {
|
|
1947
2529
|
workspaceId: input.workspaceId,
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2530
|
+
sessionIds: cancellationSessionIds,
|
|
2531
|
+
})
|
|
2532
|
+
: input.action === "pause"
|
|
2533
|
+
? await registerInterruptionWakes(db, {
|
|
2534
|
+
operationId: reserved.receipt.id,
|
|
2535
|
+
reason: "session_pause_interruption",
|
|
2536
|
+
})
|
|
2537
|
+
: await registerDescendantWakes(db, {
|
|
2538
|
+
workspaceId: input.workspaceId,
|
|
2539
|
+
sessionId: input.sessionId,
|
|
2540
|
+
reason: "session_resume",
|
|
2541
|
+
});
|
|
1951
2542
|
const [controlEvent] = await db
|
|
1952
2543
|
.insert(schema.sessionEvents)
|
|
1953
2544
|
.values({
|
|
@@ -1955,12 +2546,16 @@ export async function mutateSessionControlInTransaction(
|
|
|
1955
2546
|
workspaceId: input.workspaceId,
|
|
1956
2547
|
sessionId: input.sessionId,
|
|
1957
2548
|
sequence: updated.lastSequence + 1,
|
|
1958
|
-
type:
|
|
2549
|
+
type:
|
|
2550
|
+
input.action === "pause" || input.action === "cancel"
|
|
2551
|
+
? "session.control.paused"
|
|
2552
|
+
: "session.control.resumed",
|
|
1959
2553
|
payload: {
|
|
1960
2554
|
operationId: reserved.receipt.id,
|
|
1961
2555
|
revision,
|
|
1962
2556
|
actor,
|
|
1963
2557
|
...(input.reason ? { reason: input.reason } : {}),
|
|
2558
|
+
...(input.action === "cancel" ? { terminal: true } : {}),
|
|
1964
2559
|
interruptionCount,
|
|
1965
2560
|
},
|
|
1966
2561
|
occurredAt: new Date(),
|
|
@@ -1993,6 +2588,26 @@ export async function mutateSessionControlInTransaction(
|
|
|
1993
2588
|
: {}),
|
|
1994
2589
|
},
|
|
1995
2590
|
});
|
|
2591
|
+
const cancellation =
|
|
2592
|
+
input.action === "cancel"
|
|
2593
|
+
? await cancelSessionSubtreeInTransaction(db, {
|
|
2594
|
+
accountId: input.accountId,
|
|
2595
|
+
workspaceId: input.workspaceId,
|
|
2596
|
+
rootSessionId: input.sessionId,
|
|
2597
|
+
sessionIds: cancellationSessionIds,
|
|
2598
|
+
lockedSessions: locks.sessions,
|
|
2599
|
+
candidateTurnIds: cancellationTurnIds,
|
|
2600
|
+
lockedTurns: locks.turns,
|
|
2601
|
+
actor,
|
|
2602
|
+
reason: input.reason ?? null,
|
|
2603
|
+
operationId: reserved.receipt.id,
|
|
2604
|
+
rootControlEventId: controlEvent.id,
|
|
2605
|
+
})
|
|
2606
|
+
: {
|
|
2607
|
+
cancelledSessionCount: 0,
|
|
2608
|
+
cancelledTurnCount: 0,
|
|
2609
|
+
affectedSessionEvents: [{ sessionId: input.sessionId, eventIds: [controlEvent.id] }],
|
|
2610
|
+
};
|
|
1996
2611
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1997
2612
|
controlRevision: revision,
|
|
1998
2613
|
result: {
|
|
@@ -2000,6 +2615,9 @@ export async function mutateSessionControlInTransaction(
|
|
|
2000
2615
|
wakeCount,
|
|
2001
2616
|
eventId: controlEvent.id,
|
|
2002
2617
|
workspaceControlEventId,
|
|
2618
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
2619
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
2620
|
+
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
2003
2621
|
},
|
|
2004
2622
|
});
|
|
2005
2623
|
const control = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
@@ -2012,6 +2630,9 @@ export async function mutateSessionControlInTransaction(
|
|
|
2012
2630
|
workspaceControlEventId,
|
|
2013
2631
|
interruptionCount,
|
|
2014
2632
|
wakeCount,
|
|
2633
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
2634
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
2635
|
+
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
2015
2636
|
replay: false,
|
|
2016
2637
|
};
|
|
2017
2638
|
}
|
|
@@ -2032,6 +2653,7 @@ export async function autoResumeSessionBranchInTransaction(
|
|
|
2032
2653
|
workspaceControlEventId: string | null;
|
|
2033
2654
|
}> {
|
|
2034
2655
|
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
2656
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2035
2657
|
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
2036
2658
|
lock: "share",
|
|
2037
2659
|
});
|