Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/engines/ChatPanel/ConversationStreamProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSessionCommentsContext } from "@src/features/Org2Cloud/SessionCommen
import {
activeConversationRunnersAtom,
collectLandedTurnIds,
conversationRunnerRegistryKey,
overlayableRunnerEvents,
selectActiveRunners,
} from "@src/features/Org2Cloud/SessionConversation/activeConversationRunnersAtom";
Expand Down Expand Up @@ -204,38 +205,39 @@ export function ConversationStreamProvider({
const runnerRegistry = useAtomValue(activeConversationRunnersAtom);
const setRunnerRegistry = useSetAtom(activeConversationRunnersAtom);
const planeRootId = target?.sessionId ?? null;
const planeKey =
target && planeRootId
? conversationRunnerRegistryKey(target.orgId, planeRootId)
: null;
const landedTurnIds = useMemo(
() => collectLandedTurnIds(plane.events),
[plane.events]
);
const activeRunners = useMemo(() => {
if (!planeRootId) return [];
if (!planeKey) return [];
// Drop a runner as soon as its agent tail is on the plane — the
// authoritative rows take over with no double-render.
return selectActiveRunners(
runnerRegistry[planeRootId] ?? [],
landedTurnIds
);
}, [runnerRegistry, planeRootId, landedTurnIds]);
return selectActiveRunners(runnerRegistry[planeKey] ?? [], landedTurnIds);
}, [runnerRegistry, planeKey, landedTurnIds]);
// The in-flight runner drives the chat footer's running/typing indicator
// so a member's long turn shows "Thinking…" instead of a frozen screen.
const activeRunnerScope =
activeRunners.length > 0
? activeRunners[activeRunners.length - 1].runnerSessionId
: null;
useEffect(() => {
if (!planeRootId) return;
const list = runnerRegistry[planeRootId];
if (!planeKey) return;
const list = runnerRegistry[planeKey];
if (!list?.length) return;
const kept = selectActiveRunners(list, landedTurnIds);
if (kept.length === list.length) return;
setRunnerRegistry((current) => {
const next = { ...current };
if (kept.length === 0) delete next[planeRootId];
else next[planeRootId] = kept;
if (kept.length === 0) delete next[planeKey];
else next[planeKey] = kept;
return next;
});
}, [planeRootId, runnerRegistry, landedTurnIds, setRunnerRegistry]);
}, [planeKey, runnerRegistry, landedTurnIds, setRunnerRegistry]);
const [runnerEventsById, setRunnerEventsById] = useState<
ReadonlyMap<string, readonly SessionEvent[]>
>(() => new Map());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,11 @@ export function useImportedSessionSubmitOverride({
const runnerSessionId = liveRunnerSessionId;
if (!runnerSessionId) return;
liveRunnerSessionId = null;
settleCloudConversationRunner(planeInfo.rootId, runnerSessionId);
settleCloudConversationRunner(
planeInfo.orgId,
planeInfo.rootId,
runnerSessionId
);
};
const turnPromise = runConversationTurn({
getAccessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ import type { SessionEvent } from "@src/engines/SessionCore/core/types";

import {
collectLandedTurnIds,
conversationRunnerRegistryKey,
overlayableRunnerEvents,
selectActiveRunners,
} from "./activeConversationRunnersAtom";

describe("conversationRunnerRegistryKey", () => {
it("isolates the same root id in different organizations", () => {
expect(conversationRunnerRegistryKey("org-a", "root")).not.toBe(
conversationRunnerRegistryKey("org-b", "root")
);
});
});

const row = (turnId: string, source: "user" | "assistant" | "system") => ({
turnId,
event: { source },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ function dedupeByTurnId(
return kept.reverse();
}

/** plane rootSessionId → this device's in-flight member runners. */
/** Canonical in-memory plane identity. Root ids are not globally unique. */
export function conversationRunnerRegistryKey(
orgId: string,
rootSessionId: string
): string {
return JSON.stringify([orgId, rootSessionId]);
}

/** (organization, plane rootSessionId) → this device's in-flight runners. */
const runnerRegistryStateAtom = atom<RunnerRegistry>({});
export const activeConversationRunnersAtom = atom(
(get) => get(runnerRegistryStateAtom),
Expand All @@ -59,8 +67,8 @@ export const activeConversationRunnersAtom = atom(
const current = get(runnerRegistryStateAtom);
const proposed = typeof update === "function" ? update(current) : update;
const next: RunnerRegistry = {};
for (const [rootSessionId, runners] of Object.entries(proposed)) {
next[rootSessionId] = dedupeByTurnId(runners);
for (const [planeKey, runners] of Object.entries(proposed)) {
next[planeKey] = dedupeByTurnId(runners);
}
set(runnerRegistryStateAtom, next);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
type ConversationEventWindow,
listConversationEventsFrom,
} from "../org2CloudConversationEventsClient";
import { activeConversationRunnersAtom } from "./activeConversationRunnersAtom";
import {
activeConversationRunnersAtom,
conversationRunnerRegistryKey,
} from "./activeConversationRunnersAtom";
import {
bumpConversationPlaneSignal,
conversationPlaneSignalAtom,
Expand Down Expand Up @@ -147,6 +150,10 @@ export function registerCloudConversationRunner(input: {
turnIntentId: string;
}): void {
const store = getInstrumentedStore();
const planeKey = conversationRunnerRegistryKey(
input.orgId,
input.rootSessionId
);
store.set(org2CloudAccessSettingsAtom, (current) =>
withCloudSessionMode(
current,
Expand All @@ -157,8 +164,8 @@ export function registerCloudConversationRunner(input: {
);
store.set(activeConversationRunnersAtom, (current) => ({
...current,
[input.rootSessionId]: [
...(current[input.rootSessionId] ?? []),
[planeKey]: [
...(current[planeKey] ?? []),
{
runnerSessionId: input.runnerSessionId,
turnId: input.turnId,
Expand All @@ -169,20 +176,22 @@ export function registerCloudConversationRunner(input: {
}

export function settleCloudConversationRunner(
orgId: string,
rootSessionId: string,
runnerSessionId: string
): void {
const store = getInstrumentedStore();
const planeKey = conversationRunnerRegistryKey(orgId, rootSessionId);
store.set(activeConversationRunnersAtom, (current) => {
const list = current[rootSessionId];
const list = current[planeKey];
if (!list) return current;
const kept = list.filter(
(runner) => runner.runnerSessionId !== runnerSessionId
);
if (kept.length === list.length) return current;
const next = { ...current };
if (kept.length === 0) delete next[rootSessionId];
else next[rootSessionId] = kept;
if (kept.length === 0) delete next[planeKey];
else next[planeKey] = kept;
return next;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ConversationContinuationRecord } from "./conversationContinuation"
import { decideContinuation } from "./conversationContinuation";

const established: ConversationContinuationRecord = {
episodeId: "conversation-episode:runner-1",
continuationSessionId: "runner-1",
readThroughPlaneSeq: 12,
established: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/** Persistent local execution episode for a shared conversation. */
import {
type ConversationContinuationInput,
type ConversationContinuationLineage,
type ConversationContinuationRecord,
advanceStoredContinuationReadThrough,
clearStoredContinuation,
loadStoredContinuation,
loadStoredContinuationLineage,
markStoredContinuationEstablished,
prepareStoredContinuation,
retireStoredContinuation,
saveStoredContinuation,
} from "./conversationExecutionStore";

export type { ConversationContinuationRecord };
export type { ConversationContinuationLineage, ConversationContinuationRecord };

export function loadContinuation(
executorScope: string,
Expand All @@ -22,7 +26,7 @@ export function loadContinuation(
export function saveContinuation(
executorScope: string,
rootSessionId: string,
record: Omit<ConversationContinuationRecord, "updatedAt">,
record: ConversationContinuationInput,
backing?: Storage | null
): void {
saveStoredContinuation(executorScope, rootSessionId, record, backing);
Expand All @@ -31,7 +35,7 @@ export function saveContinuation(
export function prepareContinuation(
executorScope: string,
rootSessionId: string,
record: Omit<ConversationContinuationRecord, "updatedAt">,
record: ConversationContinuationInput,
preparedAt: string,
backing?: Storage | null
): void {
Expand All @@ -44,6 +48,30 @@ export function prepareContinuation(
);
}

export function loadContinuationLineage(
executorScope: string,
rootSessionId: string,
backing?: Storage | null
): ConversationContinuationLineage | null {
return loadStoredContinuationLineage(executorScope, rootSessionId, backing);
}

export function retireContinuation(
executorScope: string,
rootSessionId: string,
rollReason: string,
failed = false,
backing?: Storage | null
): void {
retireStoredContinuation(
executorScope,
rootSessionId,
rollReason,
failed ? "failed" : "retired",
backing
);
}

export function clearContinuation(
executorScope: string,
rootSessionId: string,
Expand Down
Loading
Loading