@opengeni/db 0.22.2 → 0.23.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-3UCHDMKG.js} +656 -282
- package/dist/chunk-3UCHDMKG.js.map +1 -0
- package/dist/{chunk-BNGEN5QZ.js → chunk-L6ADMZHE.js} +24 -2
- package/dist/chunk-L6ADMZHE.js.map +1 -0
- package/dist/index.d.ts +39 -3
- package/dist/index.js +7009 -3882
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +1416 -92
- package/dist/schema.js +11 -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 +30 -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 +239 -0
- package/dist/workspace-instruction-policies.d.ts +44 -0
- 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/package.json +4 -4
- package/src/index.ts +614 -78
- package/src/preference-registry.ts +11 -6
- package/src/provision-roles.ts +12 -0
- package/src/runtime-posture.ts +10 -0
- package/src/schema.ts +400 -43
- package/src/session-control.ts +596 -21
- package/src/session-queue-commands.ts +76 -7
- package/src/session-realtime-context.ts +393 -0
- package/src/session-realtime-ledger.ts +1790 -0
- package/src/session-realtime-mirror.ts +160 -0
- package/src/session-realtime-state.ts +25 -0
- package/src/session-realtime-terminal.ts +306 -0
- package/src/session-realtime.ts +611 -0
- package/src/workspace-instruction-policies-schema.ts +41 -0
- package/src/workspace-instruction-policies.ts +131 -2
- 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,7 @@ 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";
|
|
11
13
|
|
|
12
14
|
export const SESSION_ANCESTRY_LIMIT = 10_000;
|
|
13
15
|
|
|
@@ -1575,6 +1577,55 @@ async function registerInterruptionWakes(
|
|
|
1575
1577
|
return Number(rows[0]?.wakeCount ?? 0);
|
|
1576
1578
|
}
|
|
1577
1579
|
|
|
1580
|
+
/**
|
|
1581
|
+
* Terminal cancellation must wake every affected workflow, including sessions
|
|
1582
|
+
* parked at an approval, capacity, or other no-active-attempt boundary. Those
|
|
1583
|
+
* sessions have no new interruption row, so the ordinary Pause wake selector
|
|
1584
|
+
* cannot discover them. Signal-with-start is intentional here: a closed
|
|
1585
|
+
* workflow cheaply re-reads the terminal row and exits, while an open workflow
|
|
1586
|
+
* is released from an otherwise unbounded durable wait.
|
|
1587
|
+
*/
|
|
1588
|
+
async function registerCancellationWakes(
|
|
1589
|
+
db: Database,
|
|
1590
|
+
input: { workspaceId: string; sessionIds: string[] },
|
|
1591
|
+
): Promise<number> {
|
|
1592
|
+
if (input.sessionIds.length === 0) return 0;
|
|
1593
|
+
const sessionIds = sql.join(
|
|
1594
|
+
input.sessionIds.map((id) => sql`${id}::uuid`),
|
|
1595
|
+
sql`, `,
|
|
1596
|
+
);
|
|
1597
|
+
const rows = await db.execute<{ wakeCount: number | string }>(sql`
|
|
1598
|
+
with eligible as (
|
|
1599
|
+
select
|
|
1600
|
+
session.id as session_id,
|
|
1601
|
+
session.account_id,
|
|
1602
|
+
session.workspace_id,
|
|
1603
|
+
coalesce(session.temporal_workflow_id, 'session-' || session.id::text)
|
|
1604
|
+
as temporal_workflow_id
|
|
1605
|
+
from ${schema.sessions} session
|
|
1606
|
+
where session.workspace_id = ${input.workspaceId}
|
|
1607
|
+
and session.id in (${sessionIds})
|
|
1608
|
+
), upserted as (
|
|
1609
|
+
insert into ${schema.sessionWorkflowWakeOutbox} (
|
|
1610
|
+
session_id, account_id, workspace_id, temporal_workflow_id, reason
|
|
1611
|
+
)
|
|
1612
|
+
select session_id, account_id, workspace_id, temporal_workflow_id, 'session_cancelled'
|
|
1613
|
+
from eligible
|
|
1614
|
+
on conflict (session_id) do update set
|
|
1615
|
+
wake_revision = ${schema.sessionWorkflowWakeOutbox}.wake_revision + 1,
|
|
1616
|
+
temporal_workflow_id = excluded.temporal_workflow_id,
|
|
1617
|
+
reason = excluded.reason,
|
|
1618
|
+
attempts = 0,
|
|
1619
|
+
next_attempt_at = now(),
|
|
1620
|
+
last_error = null,
|
|
1621
|
+
updated_at = now()
|
|
1622
|
+
returning session_id
|
|
1623
|
+
)
|
|
1624
|
+
select count(*)::bigint as "wakeCount" from upserted
|
|
1625
|
+
`);
|
|
1626
|
+
return Number(rows[0]?.wakeCount ?? 0);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1578
1629
|
/** Register one exact post-commit Temporal nudge without encoding eligibility in
|
|
1579
1630
|
* the transport. The workflow re-reads canonical Postgres state; coalescing is
|
|
1580
1631
|
* revisioned so a lost or stale delivery cannot hide a later mutation. */
|
|
@@ -1786,9 +1837,452 @@ export type SessionControlMutationResult = {
|
|
|
1786
1837
|
workspaceControlEventId: string;
|
|
1787
1838
|
interruptionCount: number;
|
|
1788
1839
|
wakeCount: number;
|
|
1840
|
+
cancelledSessionCount: number;
|
|
1841
|
+
cancelledTurnCount: number;
|
|
1842
|
+
affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }>;
|
|
1789
1843
|
replay: boolean;
|
|
1790
1844
|
};
|
|
1791
1845
|
|
|
1846
|
+
async function loadSessionSubtreeIds(
|
|
1847
|
+
db: Database,
|
|
1848
|
+
workspaceId: string,
|
|
1849
|
+
rootSessionId: string,
|
|
1850
|
+
): Promise<{ sessionIds: string[]; rootParentSessionId: string | null }> {
|
|
1851
|
+
const rows: Array<{
|
|
1852
|
+
id: string;
|
|
1853
|
+
rootParentSessionId: string | null;
|
|
1854
|
+
depth: number | string;
|
|
1855
|
+
cycle: boolean;
|
|
1856
|
+
}> = await db.execute(sql`
|
|
1857
|
+
with recursive subtree(id, root_parent_session_id, depth, path, cycle) as (
|
|
1858
|
+
select session.id, session.parent_session_id, 0::integer, array[session.id]::uuid[], false
|
|
1859
|
+
from ${schema.sessions} session
|
|
1860
|
+
where session.workspace_id = ${workspaceId} and session.id = ${rootSessionId}
|
|
1861
|
+
union all
|
|
1862
|
+
select child.id, parent.root_parent_session_id, parent.depth + 1,
|
|
1863
|
+
parent.path || child.id, child.id = any(parent.path)
|
|
1864
|
+
from subtree parent
|
|
1865
|
+
join ${schema.sessions} child
|
|
1866
|
+
on child.workspace_id = ${workspaceId} and child.parent_session_id = parent.id
|
|
1867
|
+
where parent.depth < ${SESSION_ANCESTRY_LIMIT} and not parent.cycle
|
|
1868
|
+
)
|
|
1869
|
+
select id, root_parent_session_id as "rootParentSessionId", depth, cycle
|
|
1870
|
+
from subtree
|
|
1871
|
+
order by id
|
|
1872
|
+
`);
|
|
1873
|
+
if (rows.length === 0) {
|
|
1874
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} does not exist`);
|
|
1875
|
+
}
|
|
1876
|
+
if (rows.some((row) => row.cycle || Number(row.depth) >= SESSION_ANCESTRY_LIMIT)) {
|
|
1877
|
+
throw new SessionControlInvariantError(`Session ${rootSessionId} subtree is invalid`);
|
|
1878
|
+
}
|
|
1879
|
+
return {
|
|
1880
|
+
sessionIds: rows.map((row) => row.id),
|
|
1881
|
+
rootParentSessionId: rows[0]?.rootParentSessionId ?? null,
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const TERMINAL_CANCELLATION_LIVE_TURN_STATUSES = [
|
|
1886
|
+
"queued",
|
|
1887
|
+
"running",
|
|
1888
|
+
"requires_action",
|
|
1889
|
+
"recovering",
|
|
1890
|
+
"waiting_capacity",
|
|
1891
|
+
];
|
|
1892
|
+
|
|
1893
|
+
async function loadCancellationTurnIds(
|
|
1894
|
+
db: Database,
|
|
1895
|
+
workspaceId: string,
|
|
1896
|
+
sessionIds: string[],
|
|
1897
|
+
): Promise<string[]> {
|
|
1898
|
+
const rows = await db
|
|
1899
|
+
.select({ id: schema.sessionTurns.id })
|
|
1900
|
+
.from(schema.sessionTurns)
|
|
1901
|
+
.where(
|
|
1902
|
+
and(
|
|
1903
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
1904
|
+
inArray(schema.sessionTurns.sessionId, sessionIds),
|
|
1905
|
+
inArray(schema.sessionTurns.status, TERMINAL_CANCELLATION_LIVE_TURN_STATUSES),
|
|
1906
|
+
),
|
|
1907
|
+
)
|
|
1908
|
+
.orderBy(schema.sessionTurns.id);
|
|
1909
|
+
return rows.map((row) => row.id);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
async function enqueueCancelledChildOutboxInTransaction(
|
|
1913
|
+
db: Database,
|
|
1914
|
+
input: {
|
|
1915
|
+
workspaceId: string;
|
|
1916
|
+
rootSession: typeof schema.sessions.$inferSelect;
|
|
1917
|
+
parentSession: typeof schema.sessions.$inferSelect | null;
|
|
1918
|
+
},
|
|
1919
|
+
): Promise<void> {
|
|
1920
|
+
const parentSessionId = input.rootSession.parentSessionId;
|
|
1921
|
+
if (!parentSessionId) return;
|
|
1922
|
+
if (!input.parentSession || input.parentSession.id !== parentSessionId) {
|
|
1923
|
+
throw new SessionControlInvariantError(
|
|
1924
|
+
`Parent session ${parentSessionId} was not locked with cancellation root ${input.rootSession.id}`,
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
if (input.parentSession.status === "cancelled") return;
|
|
1928
|
+
let personalConnectionDelegations: (typeof schema.sessionTurns.$inferSelect)["personalConnectionDelegations"] =
|
|
1929
|
+
[];
|
|
1930
|
+
if (input.rootSession.parentTurnId) {
|
|
1931
|
+
const [parentTurn] = await db
|
|
1932
|
+
.select({ delegations: schema.sessionTurns.personalConnectionDelegations })
|
|
1933
|
+
.from(schema.sessionTurns)
|
|
1934
|
+
.where(
|
|
1935
|
+
and(
|
|
1936
|
+
eq(schema.sessionTurns.workspaceId, input.workspaceId),
|
|
1937
|
+
eq(schema.sessionTurns.sessionId, parentSessionId),
|
|
1938
|
+
eq(schema.sessionTurns.id, input.rootSession.parentTurnId),
|
|
1939
|
+
),
|
|
1940
|
+
)
|
|
1941
|
+
.limit(1);
|
|
1942
|
+
if (parentTurn) {
|
|
1943
|
+
const parsed = McpPersonalConnectionDelegations.safeParse(parentTurn.delegations);
|
|
1944
|
+
if (!parsed.success) {
|
|
1945
|
+
throw new SessionControlInvariantError(
|
|
1946
|
+
`Invalid personal MCP delegation snapshot at session_turns:${input.workspaceId}:${parentSessionId}:${input.rootSession.parentTurnId}`,
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
personalConnectionDelegations = parsed.data.map((delegation) => ({ ...delegation }));
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
await db
|
|
1953
|
+
.insert(schema.sessionSystemUpdateOutbox)
|
|
1954
|
+
.values({
|
|
1955
|
+
accountId: input.rootSession.accountId,
|
|
1956
|
+
workspaceId: input.workspaceId,
|
|
1957
|
+
sourceSessionId: input.rootSession.id,
|
|
1958
|
+
targetSessionId: parentSessionId,
|
|
1959
|
+
dedupeKey: `child-completion:${input.rootSession.id}:cancelled`,
|
|
1960
|
+
kind: "child_terminal_result",
|
|
1961
|
+
classification: "info",
|
|
1962
|
+
sourceId: input.rootSession.id,
|
|
1963
|
+
summary: "Child session was terminally cancelled.",
|
|
1964
|
+
payload: {
|
|
1965
|
+
type: "child_terminal_result",
|
|
1966
|
+
childSessionId: input.rootSession.id,
|
|
1967
|
+
status: "cancelled",
|
|
1968
|
+
},
|
|
1969
|
+
lineage: {
|
|
1970
|
+
childSessionId: input.rootSession.id,
|
|
1971
|
+
parentSessionId,
|
|
1972
|
+
...(input.rootSession.parentTurnId ? { parentTurnId: input.rootSession.parentTurnId } : {}),
|
|
1973
|
+
},
|
|
1974
|
+
personalConnectionDelegations,
|
|
1975
|
+
})
|
|
1976
|
+
.onConflictDoNothing({
|
|
1977
|
+
target: [
|
|
1978
|
+
schema.sessionSystemUpdateOutbox.workspaceId,
|
|
1979
|
+
schema.sessionSystemUpdateOutbox.dedupeKey,
|
|
1980
|
+
],
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
async function assertSessionBranchIsNotCancelled(
|
|
1985
|
+
db: Database,
|
|
1986
|
+
workspaceId: string,
|
|
1987
|
+
sessionId: string,
|
|
1988
|
+
): Promise<void> {
|
|
1989
|
+
const rows = await db.execute<{
|
|
1990
|
+
cancelled: boolean;
|
|
1991
|
+
invalid: boolean;
|
|
1992
|
+
}>(sql`
|
|
1993
|
+
with recursive ancestry(id, parent_id, status, depth, path, cycle) as (
|
|
1994
|
+
select session.id, session.parent_session_id, session.status, 0::integer,
|
|
1995
|
+
array[session.id]::uuid[], false
|
|
1996
|
+
from ${schema.sessions} session
|
|
1997
|
+
where session.workspace_id = ${workspaceId} and session.id = ${sessionId}
|
|
1998
|
+
union all
|
|
1999
|
+
select parent.id, parent.parent_session_id, parent.status, child.depth + 1,
|
|
2000
|
+
child.path || parent.id, parent.id = any(child.path)
|
|
2001
|
+
from ancestry child
|
|
2002
|
+
join ${schema.sessions} parent
|
|
2003
|
+
on parent.workspace_id = ${workspaceId} and parent.id = child.parent_id
|
|
2004
|
+
where child.depth < ${SESSION_ANCESTRY_LIMIT} and not child.cycle
|
|
2005
|
+
)
|
|
2006
|
+
select
|
|
2007
|
+
coalesce(bool_or(status = 'cancelled'), false) as cancelled,
|
|
2008
|
+
count(*) = 0 or coalesce(bool_or(cycle), false)
|
|
2009
|
+
or coalesce(max(depth), 0) >= ${SESSION_ANCESTRY_LIMIT} as invalid
|
|
2010
|
+
from ancestry
|
|
2011
|
+
`);
|
|
2012
|
+
if (rows[0]?.invalid) {
|
|
2013
|
+
throw new SessionControlInvariantError(`Session ${sessionId} ancestry is invalid`);
|
|
2014
|
+
}
|
|
2015
|
+
if (rows[0]?.cancelled) {
|
|
2016
|
+
throw new SessionControlConflictError("Cancelled session subtree cannot accept work");
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function cancelSessionSubtreeInTransaction(
|
|
2021
|
+
db: Database,
|
|
2022
|
+
input: {
|
|
2023
|
+
accountId: string;
|
|
2024
|
+
workspaceId: string;
|
|
2025
|
+
rootSessionId: string;
|
|
2026
|
+
sessionIds: string[];
|
|
2027
|
+
lockedSessions: Array<typeof schema.sessions.$inferSelect>;
|
|
2028
|
+
candidateTurnIds: string[];
|
|
2029
|
+
lockedTurns: Array<typeof schema.sessionTurns.$inferSelect>;
|
|
2030
|
+
actor: string;
|
|
2031
|
+
reason: string | null;
|
|
2032
|
+
operationId: string;
|
|
2033
|
+
rootControlEventId: string;
|
|
2034
|
+
},
|
|
2035
|
+
): Promise<{
|
|
2036
|
+
cancelledSessionCount: number;
|
|
2037
|
+
cancelledTurnCount: number;
|
|
2038
|
+
affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }>;
|
|
2039
|
+
}> {
|
|
2040
|
+
const candidateTurnIdSet = new Set(input.candidateTurnIds);
|
|
2041
|
+
const candidateTurns = input.lockedTurns.filter((turn) => candidateTurnIdSet.has(turn.id));
|
|
2042
|
+
if (candidateTurns.length !== candidateTurnIdSet.size) {
|
|
2043
|
+
throw new SessionControlInvariantError("Cancellation candidate turns changed while locking");
|
|
2044
|
+
}
|
|
2045
|
+
// The outer command acquired every session and candidate turn in one UUID-
|
|
2046
|
+
// sorted call before the actor attempt. Repeat only those already-held locks
|
|
2047
|
+
// so this event-writing suffix retains a directly auditable lock contract;
|
|
2048
|
+
// this call must never discover or acquire a new row.
|
|
2049
|
+
const suffixLocks = await lockSessionEventWriteRows(db, {
|
|
2050
|
+
workspaceId: input.workspaceId,
|
|
2051
|
+
controlLock: "already_locked",
|
|
2052
|
+
workspaceLock: "already_locked",
|
|
2053
|
+
sessionIds: input.lockedSessions.map((session) => session.id),
|
|
2054
|
+
turnIds: input.candidateTurnIds,
|
|
2055
|
+
});
|
|
2056
|
+
if (
|
|
2057
|
+
suffixLocks.sessions.length !== input.lockedSessions.length ||
|
|
2058
|
+
suffixLocks.turns.length !== candidateTurnIdSet.size
|
|
2059
|
+
) {
|
|
2060
|
+
throw new SessionControlInvariantError("Cancellation rows changed under canonical locks");
|
|
2061
|
+
}
|
|
2062
|
+
const sessionIdSet = new Set(input.sessionIds);
|
|
2063
|
+
const rootSession = input.lockedSessions.find((session) => session.id === input.rootSessionId);
|
|
2064
|
+
if (!rootSession) {
|
|
2065
|
+
throw new SessionControlInvariantError(
|
|
2066
|
+
`Cancellation root ${input.rootSessionId} was not locked with its subtree`,
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
const rootParentSession = rootSession.parentSessionId
|
|
2070
|
+
? (input.lockedSessions.find((session) => session.id === rootSession.parentSessionId) ?? null)
|
|
2071
|
+
: null;
|
|
2072
|
+
const candidateTurnById = new Map(candidateTurns.map((turn) => [turn.id, turn]));
|
|
2073
|
+
const liveTurnIds = new Set(
|
|
2074
|
+
input.lockedSessions
|
|
2075
|
+
.filter((session) => session.activeTurnId !== null)
|
|
2076
|
+
.flatMap((session) => {
|
|
2077
|
+
const turn = session.activeTurnId ? candidateTurnById.get(session.activeTurnId) : undefined;
|
|
2078
|
+
if (turn?.activeAttemptId === null) return [];
|
|
2079
|
+
return turn ? [turn.id] : [];
|
|
2080
|
+
}),
|
|
2081
|
+
);
|
|
2082
|
+
const immediatelyCancelledTurns = candidateTurns.filter((turn) => !liveTurnIds.has(turn.id));
|
|
2083
|
+
const cancelledTurnsBySession = new Map<string, typeof immediatelyCancelledTurns>();
|
|
2084
|
+
for (const turn of immediatelyCancelledTurns) {
|
|
2085
|
+
const turns = cancelledTurnsBySession.get(turn.sessionId);
|
|
2086
|
+
if (turns) turns.push(turn);
|
|
2087
|
+
else cancelledTurnsBySession.set(turn.sessionId, [turn]);
|
|
2088
|
+
}
|
|
2089
|
+
const immediatelyCancelledTurnIds = immediatelyCancelledTurns.map((turn) => turn.id);
|
|
2090
|
+
const now = new Date();
|
|
2091
|
+
let cancelledHumanInputs: Array<{ id: string; sessionId: string; turnId: string }> = [];
|
|
2092
|
+
if (immediatelyCancelledTurnIds.length > 0) {
|
|
2093
|
+
await db
|
|
2094
|
+
.update(schema.sessionTurns)
|
|
2095
|
+
.set({
|
|
2096
|
+
status: "cancelled",
|
|
2097
|
+
activeAttemptId: null,
|
|
2098
|
+
cancelledBy: input.actor,
|
|
2099
|
+
cancelReason: input.reason ?? "session_cancelled",
|
|
2100
|
+
version: sql`${schema.sessionTurns.version} + 1`,
|
|
2101
|
+
finishedAt: now,
|
|
2102
|
+
updatedAt: now,
|
|
2103
|
+
})
|
|
2104
|
+
.where(inArray(schema.sessionTurns.id, immediatelyCancelledTurnIds));
|
|
2105
|
+
cancelledHumanInputs = await db
|
|
2106
|
+
.update(schema.sessionHumanInputRequests)
|
|
2107
|
+
.set({
|
|
2108
|
+
status: "cancelled",
|
|
2109
|
+
response: { outcome: "cancelled" },
|
|
2110
|
+
respondedBy: input.actor,
|
|
2111
|
+
respondedAt: now,
|
|
2112
|
+
updatedAt: now,
|
|
2113
|
+
})
|
|
2114
|
+
.where(
|
|
2115
|
+
and(
|
|
2116
|
+
eq(schema.sessionHumanInputRequests.workspaceId, input.workspaceId),
|
|
2117
|
+
inArray(schema.sessionHumanInputRequests.turnId, immediatelyCancelledTurnIds),
|
|
2118
|
+
eq(schema.sessionHumanInputRequests.status, "pending"),
|
|
2119
|
+
),
|
|
2120
|
+
)
|
|
2121
|
+
.returning({
|
|
2122
|
+
id: schema.sessionHumanInputRequests.id,
|
|
2123
|
+
sessionId: schema.sessionHumanInputRequests.sessionId,
|
|
2124
|
+
turnId: schema.sessionHumanInputRequests.turnId,
|
|
2125
|
+
});
|
|
2126
|
+
await db
|
|
2127
|
+
.update(schema.codexCapacityWaiters)
|
|
2128
|
+
.set({ status: "superseded", lastWakeReason: "session_cancelled", updatedAt: now })
|
|
2129
|
+
.where(
|
|
2130
|
+
and(
|
|
2131
|
+
eq(schema.codexCapacityWaiters.workspaceId, input.workspaceId),
|
|
2132
|
+
inArray(schema.codexCapacityWaiters.blockedTurnId, immediatelyCancelledTurnIds),
|
|
2133
|
+
eq(schema.codexCapacityWaiters.status, "waiting"),
|
|
2134
|
+
),
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
const cancelledSystemUpdates = await db
|
|
2138
|
+
.update(schema.sessionSystemUpdates)
|
|
2139
|
+
.set({ state: "cancelled" })
|
|
2140
|
+
.where(
|
|
2141
|
+
and(
|
|
2142
|
+
eq(schema.sessionSystemUpdates.workspaceId, input.workspaceId),
|
|
2143
|
+
inArray(schema.sessionSystemUpdates.sessionId, input.sessionIds),
|
|
2144
|
+
eq(schema.sessionSystemUpdates.state, "pending"),
|
|
2145
|
+
),
|
|
2146
|
+
)
|
|
2147
|
+
.returning({
|
|
2148
|
+
id: schema.sessionSystemUpdates.id,
|
|
2149
|
+
sessionId: schema.sessionSystemUpdates.sessionId,
|
|
2150
|
+
});
|
|
2151
|
+
const humanInputsByTurn = new Map<string, typeof cancelledHumanInputs>();
|
|
2152
|
+
for (const humanInput of cancelledHumanInputs) {
|
|
2153
|
+
const inputs = humanInputsByTurn.get(humanInput.turnId);
|
|
2154
|
+
if (inputs) inputs.push(humanInput);
|
|
2155
|
+
else humanInputsByTurn.set(humanInput.turnId, [humanInput]);
|
|
2156
|
+
}
|
|
2157
|
+
const systemUpdatesBySession = new Map<string, typeof cancelledSystemUpdates>();
|
|
2158
|
+
for (const update of cancelledSystemUpdates) {
|
|
2159
|
+
const updates = systemUpdatesBySession.get(update.sessionId);
|
|
2160
|
+
if (updates) updates.push(update);
|
|
2161
|
+
else systemUpdatesBySession.set(update.sessionId, [update]);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const affectedSessionEvents: Array<{ sessionId: string; eventIds: string[] }> = [];
|
|
2165
|
+
for (const session of input.lockedSessions) {
|
|
2166
|
+
if (!sessionIdSet.has(session.id)) continue;
|
|
2167
|
+
let sequence = session.lastSequence + (session.id === input.rootSessionId ? 1 : 0);
|
|
2168
|
+
const cancelledTurns = cancelledTurnsBySession.get(session.id) ?? [];
|
|
2169
|
+
const preinsertedEvents: Array<{ id: string; sequence: number }> = [];
|
|
2170
|
+
const eventValues: Array<typeof schema.sessionEvents.$inferInsert> = [];
|
|
2171
|
+
for (const update of (systemUpdatesBySession.get(session.id) ?? []).sort((left, right) =>
|
|
2172
|
+
left.id.localeCompare(right.id),
|
|
2173
|
+
)) {
|
|
2174
|
+
eventValues.push({
|
|
2175
|
+
accountId: input.accountId,
|
|
2176
|
+
workspaceId: input.workspaceId,
|
|
2177
|
+
sessionId: session.id,
|
|
2178
|
+
sequence: ++sequence,
|
|
2179
|
+
type: "system.update.cancelled",
|
|
2180
|
+
payload: { updateId: update.id, reason: "session_cancelled" },
|
|
2181
|
+
occurredAt: now,
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
for (const turn of cancelledTurns) {
|
|
2185
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(db, {
|
|
2186
|
+
accountId: input.accountId,
|
|
2187
|
+
workspaceId: input.workspaceId,
|
|
2188
|
+
sessionId: session.id,
|
|
2189
|
+
turnId: turn.id,
|
|
2190
|
+
reason: "session_cancelled",
|
|
2191
|
+
sequence,
|
|
2192
|
+
now,
|
|
2193
|
+
});
|
|
2194
|
+
sequence = closedTools.sequence;
|
|
2195
|
+
preinsertedEvents.push(
|
|
2196
|
+
...closedTools.events.map((event) => ({ id: event.id, sequence: event.sequence })),
|
|
2197
|
+
);
|
|
2198
|
+
for (const humanInput of (humanInputsByTurn.get(turn.id) ?? []).sort((left, right) =>
|
|
2199
|
+
left.id.localeCompare(right.id),
|
|
2200
|
+
)) {
|
|
2201
|
+
eventValues.push({
|
|
2202
|
+
accountId: input.accountId,
|
|
2203
|
+
workspaceId: input.workspaceId,
|
|
2204
|
+
sessionId: session.id,
|
|
2205
|
+
sequence: ++sequence,
|
|
2206
|
+
type: "user.humanInputResponse",
|
|
2207
|
+
turnId: turn.id,
|
|
2208
|
+
turnGeneration: turn.executionGeneration,
|
|
2209
|
+
...(turn.id === session.activeTurnId ? { turnAssociation: "current" as const } : {}),
|
|
2210
|
+
payload: { requestId: humanInput.id, response: { outcome: "cancelled" } },
|
|
2211
|
+
occurredAt: now,
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
eventValues.push({
|
|
2215
|
+
accountId: input.accountId,
|
|
2216
|
+
workspaceId: input.workspaceId,
|
|
2217
|
+
sessionId: session.id,
|
|
2218
|
+
sequence: ++sequence,
|
|
2219
|
+
type: "turn.cancelled",
|
|
2220
|
+
turnId: turn.id,
|
|
2221
|
+
turnGeneration: turn.executionGeneration,
|
|
2222
|
+
...(turn.id === session.activeTurnId ? { turnAssociation: "current" as const } : {}),
|
|
2223
|
+
payload: {
|
|
2224
|
+
reason: input.reason ?? "session_cancelled",
|
|
2225
|
+
operationId: input.operationId,
|
|
2226
|
+
},
|
|
2227
|
+
occurredAt: now,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
if (session.status !== "cancelled") {
|
|
2231
|
+
eventValues.push({
|
|
2232
|
+
accountId: input.accountId,
|
|
2233
|
+
workspaceId: input.workspaceId,
|
|
2234
|
+
sessionId: session.id,
|
|
2235
|
+
sequence: ++sequence,
|
|
2236
|
+
type: "session.status.changed",
|
|
2237
|
+
payload: {
|
|
2238
|
+
status: "cancelled",
|
|
2239
|
+
reason: input.reason ?? "session_cancelled",
|
|
2240
|
+
operationId: input.operationId,
|
|
2241
|
+
},
|
|
2242
|
+
occurredAt: now,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
const inserted =
|
|
2246
|
+
eventValues.length > 0
|
|
2247
|
+
? await db.insert(schema.sessionEvents).values(eventValues).returning({
|
|
2248
|
+
id: schema.sessionEvents.id,
|
|
2249
|
+
sequence: schema.sessionEvents.sequence,
|
|
2250
|
+
})
|
|
2251
|
+
: [];
|
|
2252
|
+
const liveTurnId =
|
|
2253
|
+
session.activeTurnId && liveTurnIds.has(session.activeTurnId) ? session.activeTurnId : null;
|
|
2254
|
+
await db
|
|
2255
|
+
.update(schema.sessions)
|
|
2256
|
+
.set({
|
|
2257
|
+
status: "cancelled",
|
|
2258
|
+
activeTurnId: liveTurnId,
|
|
2259
|
+
queueVersion: sql`${schema.sessions.queueVersion} + 1`,
|
|
2260
|
+
queueHeadPosition: 0,
|
|
2261
|
+
queueTailPosition: 0,
|
|
2262
|
+
lastSequence: sequence,
|
|
2263
|
+
updatedAt: now,
|
|
2264
|
+
})
|
|
2265
|
+
.where(
|
|
2266
|
+
and(eq(schema.sessions.workspaceId, input.workspaceId), eq(schema.sessions.id, session.id)),
|
|
2267
|
+
);
|
|
2268
|
+
const eventIds = [...preinsertedEvents, ...inserted]
|
|
2269
|
+
.sort((left, right) => left.sequence - right.sequence)
|
|
2270
|
+
.map((event) => event.id);
|
|
2271
|
+
if (session.id === input.rootSessionId) eventIds.unshift(input.rootControlEventId);
|
|
2272
|
+
if (eventIds.length > 0) affectedSessionEvents.push({ sessionId: session.id, eventIds });
|
|
2273
|
+
}
|
|
2274
|
+
await enqueueCancelledChildOutboxInTransaction(db, {
|
|
2275
|
+
workspaceId: input.workspaceId,
|
|
2276
|
+
rootSession,
|
|
2277
|
+
parentSession: rootParentSession,
|
|
2278
|
+
});
|
|
2279
|
+
return {
|
|
2280
|
+
cancelledSessionCount: input.sessionIds.length,
|
|
2281
|
+
cancelledTurnCount: candidateTurns.length,
|
|
2282
|
+
affectedSessionEvents,
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1792
2286
|
export async function mutateSessionControlInTransaction(
|
|
1793
2287
|
db: Database,
|
|
1794
2288
|
input: {
|
|
@@ -1797,22 +2291,53 @@ export async function mutateSessionControlInTransaction(
|
|
|
1797
2291
|
sessionId: string;
|
|
1798
2292
|
actor: SessionCommandActor;
|
|
1799
2293
|
operationKey: string;
|
|
1800
|
-
action: "pause" | "resume";
|
|
2294
|
+
action: "pause" | "resume" | "cancel";
|
|
1801
2295
|
reason?: string | null;
|
|
1802
2296
|
expectedControlEtag?: string | null;
|
|
1803
2297
|
},
|
|
1804
2298
|
): Promise<SessionControlMutationResult> {
|
|
1805
2299
|
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
1806
|
-
|
|
2300
|
+
const cancellationSubtree =
|
|
2301
|
+
input.action === "cancel"
|
|
2302
|
+
? await loadSessionSubtreeIds(db, input.workspaceId, input.sessionId)
|
|
2303
|
+
: { sessionIds: [input.sessionId], rootParentSessionId: null };
|
|
2304
|
+
const cancellationSessionIds = cancellationSubtree.sessionIds;
|
|
2305
|
+
const cancellationTurnIds =
|
|
2306
|
+
input.action === "cancel"
|
|
2307
|
+
? await loadCancellationTurnIds(db, input.workspaceId, cancellationSessionIds)
|
|
2308
|
+
: [];
|
|
2309
|
+
const locks = await lockSessionEventWriteRows(db, {
|
|
1807
2310
|
workspaceId: input.workspaceId,
|
|
1808
2311
|
controlLock: "already_locked",
|
|
1809
2312
|
sessionIds:
|
|
1810
2313
|
input.actor.type === "agent_attempt"
|
|
1811
|
-
? [
|
|
1812
|
-
|
|
1813
|
-
|
|
2314
|
+
? [
|
|
2315
|
+
input.actor.sessionId,
|
|
2316
|
+
...cancellationSessionIds,
|
|
2317
|
+
...(cancellationSubtree.rootParentSessionId
|
|
2318
|
+
? [cancellationSubtree.rootParentSessionId]
|
|
2319
|
+
: []),
|
|
2320
|
+
]
|
|
2321
|
+
: [
|
|
2322
|
+
...cancellationSessionIds,
|
|
2323
|
+
...(cancellationSubtree.rootParentSessionId
|
|
2324
|
+
? [cancellationSubtree.rootParentSessionId]
|
|
2325
|
+
: []),
|
|
2326
|
+
],
|
|
2327
|
+
turnIds:
|
|
2328
|
+
input.actor.type === "agent_attempt"
|
|
2329
|
+
? [input.actor.turnId, ...cancellationTurnIds]
|
|
2330
|
+
: cancellationTurnIds,
|
|
1814
2331
|
attemptIds: input.actor.type === "agent_attempt" ? [input.actor.attemptId] : [],
|
|
1815
2332
|
});
|
|
2333
|
+
if (input.action === "cancel") {
|
|
2334
|
+
const rootSession = locks.sessions.find((session) => session.id === input.sessionId);
|
|
2335
|
+
if (!rootSession || rootSession.parentSessionId !== cancellationSubtree.rootParentSessionId) {
|
|
2336
|
+
throw new SessionControlInvariantError(
|
|
2337
|
+
`Session ${input.sessionId} parent changed while establishing cancellation locks`,
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
1816
2341
|
const hash = canonicalSessionCommandHash({
|
|
1817
2342
|
action: input.action,
|
|
1818
2343
|
reason: input.reason ?? null,
|
|
@@ -1848,6 +2373,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
1848
2373
|
workspaceControlEventId,
|
|
1849
2374
|
interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
|
|
1850
2375
|
wakeCount: Number(reserved.receipt.result.wakeCount ?? 0),
|
|
2376
|
+
cancelledSessionCount: Number(reserved.receipt.result.cancelledSessionCount ?? 0),
|
|
2377
|
+
cancelledTurnCount: Number(reserved.receipt.result.cancelledTurnCount ?? 0),
|
|
2378
|
+
affectedSessionEvents: Array.isArray(reserved.receipt.result.affectedSessionEvents)
|
|
2379
|
+
? (reserved.receipt.result.affectedSessionEvents as Array<{
|
|
2380
|
+
sessionId: string;
|
|
2381
|
+
eventIds: string[];
|
|
2382
|
+
}>)
|
|
2383
|
+
: [{ sessionId: input.sessionId, eventIds: [sessionControlEventId] }],
|
|
1851
2384
|
replay: true,
|
|
1852
2385
|
};
|
|
1853
2386
|
}
|
|
@@ -1856,9 +2389,12 @@ export async function mutateSessionControlInTransaction(
|
|
|
1856
2389
|
workspaceId: input.workspaceId,
|
|
1857
2390
|
actor: input.actor,
|
|
1858
2391
|
targetSessionId: input.sessionId,
|
|
1859
|
-
action: input.action,
|
|
2392
|
+
action: input.action === "cancel" ? "pause" : input.action,
|
|
1860
2393
|
});
|
|
1861
2394
|
}
|
|
2395
|
+
if (input.action === "resume") {
|
|
2396
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2397
|
+
}
|
|
1862
2398
|
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
1863
2399
|
workspaceControl: workspace,
|
|
1864
2400
|
});
|
|
@@ -1868,10 +2404,10 @@ export async function mutateSessionControlInTransaction(
|
|
|
1868
2404
|
|
|
1869
2405
|
const revision = nextRevision(workspace);
|
|
1870
2406
|
await advanceWorkspaceRevision(db, input.workspaceId, revision);
|
|
1871
|
-
const
|
|
2407
|
+
const updatedRows = await db
|
|
1872
2408
|
.update(schema.sessions)
|
|
1873
2409
|
.set(
|
|
1874
|
-
input.action === "pause"
|
|
2410
|
+
input.action === "pause" || input.action === "cancel"
|
|
1875
2411
|
? {
|
|
1876
2412
|
directControlState: "paused",
|
|
1877
2413
|
directPauseRevision: revision,
|
|
@@ -1901,13 +2437,16 @@ export async function mutateSessionControlInTransaction(
|
|
|
1901
2437
|
.where(
|
|
1902
2438
|
and(
|
|
1903
2439
|
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
1904
|
-
|
|
2440
|
+
input.action === "cancel"
|
|
2441
|
+
? inArray(schema.sessions.id, cancellationSessionIds)
|
|
2442
|
+
: eq(schema.sessions.id, input.sessionId),
|
|
1905
2443
|
),
|
|
1906
2444
|
)
|
|
1907
2445
|
.returning({
|
|
1908
2446
|
id: schema.sessions.id,
|
|
1909
2447
|
lastSequence: schema.sessions.lastSequence,
|
|
1910
2448
|
});
|
|
2449
|
+
const updated = updatedRows.find((row) => row.id === input.sessionId);
|
|
1911
2450
|
if (!updated) throw new SessionControlInvariantError(`Session ${input.sessionId} disappeared`);
|
|
1912
2451
|
|
|
1913
2452
|
const actor =
|
|
@@ -1920,14 +2459,14 @@ export async function mutateSessionControlInTransaction(
|
|
|
1920
2459
|
revision,
|
|
1921
2460
|
scope: "session",
|
|
1922
2461
|
rootSessionId: input.sessionId,
|
|
1923
|
-
action: input.action,
|
|
2462
|
+
action: input.action === "cancel" ? "pause" : input.action,
|
|
1924
2463
|
automatic: false,
|
|
1925
2464
|
reason: input.reason ?? null,
|
|
1926
2465
|
actor,
|
|
1927
2466
|
});
|
|
1928
2467
|
|
|
1929
2468
|
const interruptionCount =
|
|
1930
|
-
input.action === "pause"
|
|
2469
|
+
input.action === "pause" || input.action === "cancel"
|
|
1931
2470
|
? await interruptDescendantAttempts(db, {
|
|
1932
2471
|
accountId: input.accountId,
|
|
1933
2472
|
workspaceId: input.workspaceId,
|
|
@@ -1938,16 +2477,21 @@ export async function mutateSessionControlInTransaction(
|
|
|
1938
2477
|
})
|
|
1939
2478
|
: 0;
|
|
1940
2479
|
const wakeCount =
|
|
1941
|
-
input.action === "
|
|
1942
|
-
? await
|
|
1943
|
-
operationId: reserved.receipt.id,
|
|
1944
|
-
reason: "session_pause_interruption",
|
|
1945
|
-
})
|
|
1946
|
-
: await registerDescendantWakes(db, {
|
|
2480
|
+
input.action === "cancel"
|
|
2481
|
+
? await registerCancellationWakes(db, {
|
|
1947
2482
|
workspaceId: input.workspaceId,
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2483
|
+
sessionIds: cancellationSessionIds,
|
|
2484
|
+
})
|
|
2485
|
+
: input.action === "pause"
|
|
2486
|
+
? await registerInterruptionWakes(db, {
|
|
2487
|
+
operationId: reserved.receipt.id,
|
|
2488
|
+
reason: "session_pause_interruption",
|
|
2489
|
+
})
|
|
2490
|
+
: await registerDescendantWakes(db, {
|
|
2491
|
+
workspaceId: input.workspaceId,
|
|
2492
|
+
sessionId: input.sessionId,
|
|
2493
|
+
reason: "session_resume",
|
|
2494
|
+
});
|
|
1951
2495
|
const [controlEvent] = await db
|
|
1952
2496
|
.insert(schema.sessionEvents)
|
|
1953
2497
|
.values({
|
|
@@ -1955,12 +2499,16 @@ export async function mutateSessionControlInTransaction(
|
|
|
1955
2499
|
workspaceId: input.workspaceId,
|
|
1956
2500
|
sessionId: input.sessionId,
|
|
1957
2501
|
sequence: updated.lastSequence + 1,
|
|
1958
|
-
type:
|
|
2502
|
+
type:
|
|
2503
|
+
input.action === "pause" || input.action === "cancel"
|
|
2504
|
+
? "session.control.paused"
|
|
2505
|
+
: "session.control.resumed",
|
|
1959
2506
|
payload: {
|
|
1960
2507
|
operationId: reserved.receipt.id,
|
|
1961
2508
|
revision,
|
|
1962
2509
|
actor,
|
|
1963
2510
|
...(input.reason ? { reason: input.reason } : {}),
|
|
2511
|
+
...(input.action === "cancel" ? { terminal: true } : {}),
|
|
1964
2512
|
interruptionCount,
|
|
1965
2513
|
},
|
|
1966
2514
|
occurredAt: new Date(),
|
|
@@ -1993,6 +2541,26 @@ export async function mutateSessionControlInTransaction(
|
|
|
1993
2541
|
: {}),
|
|
1994
2542
|
},
|
|
1995
2543
|
});
|
|
2544
|
+
const cancellation =
|
|
2545
|
+
input.action === "cancel"
|
|
2546
|
+
? await cancelSessionSubtreeInTransaction(db, {
|
|
2547
|
+
accountId: input.accountId,
|
|
2548
|
+
workspaceId: input.workspaceId,
|
|
2549
|
+
rootSessionId: input.sessionId,
|
|
2550
|
+
sessionIds: cancellationSessionIds,
|
|
2551
|
+
lockedSessions: locks.sessions,
|
|
2552
|
+
candidateTurnIds: cancellationTurnIds,
|
|
2553
|
+
lockedTurns: locks.turns,
|
|
2554
|
+
actor,
|
|
2555
|
+
reason: input.reason ?? null,
|
|
2556
|
+
operationId: reserved.receipt.id,
|
|
2557
|
+
rootControlEventId: controlEvent.id,
|
|
2558
|
+
})
|
|
2559
|
+
: {
|
|
2560
|
+
cancelledSessionCount: 0,
|
|
2561
|
+
cancelledTurnCount: 0,
|
|
2562
|
+
affectedSessionEvents: [{ sessionId: input.sessionId, eventIds: [controlEvent.id] }],
|
|
2563
|
+
};
|
|
1996
2564
|
const receipt = await updateSessionCommandReceiptResult(db, reserved.receipt.id, {
|
|
1997
2565
|
controlRevision: revision,
|
|
1998
2566
|
result: {
|
|
@@ -2000,6 +2568,9 @@ export async function mutateSessionControlInTransaction(
|
|
|
2000
2568
|
wakeCount,
|
|
2001
2569
|
eventId: controlEvent.id,
|
|
2002
2570
|
workspaceControlEventId,
|
|
2571
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
2572
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
2573
|
+
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
2003
2574
|
},
|
|
2004
2575
|
});
|
|
2005
2576
|
const control = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
@@ -2012,6 +2583,9 @@ export async function mutateSessionControlInTransaction(
|
|
|
2012
2583
|
workspaceControlEventId,
|
|
2013
2584
|
interruptionCount,
|
|
2014
2585
|
wakeCount,
|
|
2586
|
+
cancelledSessionCount: cancellation.cancelledSessionCount,
|
|
2587
|
+
cancelledTurnCount: cancellation.cancelledTurnCount,
|
|
2588
|
+
affectedSessionEvents: cancellation.affectedSessionEvents,
|
|
2015
2589
|
replay: false,
|
|
2016
2590
|
};
|
|
2017
2591
|
}
|
|
@@ -2032,6 +2606,7 @@ export async function autoResumeSessionBranchInTransaction(
|
|
|
2032
2606
|
workspaceControlEventId: string | null;
|
|
2033
2607
|
}> {
|
|
2034
2608
|
const workspace = await lockWorkspaceInferenceControl(db, input.workspaceId, "update");
|
|
2609
|
+
await assertSessionBranchIsNotCancelled(db, input.workspaceId, input.sessionId);
|
|
2035
2610
|
const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
|
|
2036
2611
|
lock: "share",
|
|
2037
2612
|
});
|