Skip to content
Open
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
20 changes: 8 additions & 12 deletions apps/mobile/src/state/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,26 +210,22 @@ export function useSideChats(ref: ScopedThreadRef) {

export function useLiveThreadLinkLabels(markdown: string, environmentId: EnvironmentId): string {
const refs = hasThreadLinks(markdown) ? threadLinkRefs(markdown, environmentId) : [];
const refsKey = JSON.stringify(refs.map(threadKey));
const titles = useAtomValue(
refs.length === 0
? EMPTY_THREAD_LINK_TITLES_ATOM
: threadLinkTitlesAtom(JSON.stringify(refs.map(threadKey))),
refs.length === 0 ? EMPTY_THREAD_LINK_TITLES_ATOM : threadLinkTitlesAtom(refsKey),
);
// Shells leave out idle subagent children and older settled threads, so a cited thread the
// shell lacks is read with its family until it lands; only a settled empty read proves it gone.
const missingKey = JSON.stringify(
refs.filter((ref) => !titles.has(threadKey(ref))).map(threadKey),
);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- mounts family reads for cited threads the shell omits while this message shows them
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- keeps cited rows mounted for the message's lifetime, independently of the family data it loads
useEffect(() => {
const releases = (JSON.parse(missingKey) as ReadonlyArray<string>).map((key) => {
const releases = decodeThreadKeys(JSON.parse(refsKey)).map((key) => {
const ref = scopeThreadRef(...decodeThreadKey(JSON.parse(key)));
return threadFamilies.mount(appAtomRegistry, ref.environmentId, ref.threadId);
return threadFamilies.mount(appAtomRegistry, ref.environmentId, ref.threadId, {
onlyWhenMissing: true,
});
});
return () => {
for (const release of releases) release();
};
}, [missingKey]);
}, [refsKey]);
return relabelThreadLinks(markdown, environmentId, (ref) => titles.get(threadKey(ref)));
}

Expand Down
103 changes: 102 additions & 1 deletion apps/server/src/orchestration-v2/ThreadSplitSuggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import {
ProjectId,
ProviderInstanceId,
MessageId,
RunId,
DEFAULT_SERVER_SETTINGS,
type OrchestrationV2AppThread,
type OrchestrationV2ConversationMessage,
type OrchestrationV2Run,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientResponse from "effect/http/HttpClientResponse";
Expand All @@ -21,9 +24,16 @@ import * as ServerSettings from "../serverSettings.ts";
import * as Suggestions from "./ThreadSplitSuggestions.ts";
import type { ProjectionRecords, ProjectionRecordField } from "./ProjectionStore.ts";

const decodeRequest = Schema.decodeUnknownSync(
Schema.fromJsonString(Schema.Struct({ state: Schema.Unknown })),
);
const threadId = ThreadId.make("split-test");
const input = { threadId, message: "What is the capital of France?" };
function source() {
function source(): {
thread: OrchestrationV2AppThread;
messages: OrchestrationV2ConversationMessage[];
runs: OrchestrationV2Run[];
} {
const now = DateTime.makeUnsafe("2026-11-03T00:00:00Z");
const thread: OrchestrationV2AppThread = {
id: threadId,
Expand Down Expand Up @@ -193,3 +203,94 @@ it.effect("does not queue a second model request while one is in flight", () =>
}).pipe(Effect.provide(layer));
}),
);

it.effect("classifies only history from visible turns", () =>
Effect.gen(function* () {
for (const scenario of [
"rolled-back",
"cancelled-before-start",
"cancelled-after-start",
] as const) {
const records = source();
const runId = RunId.make("discarded-run");
const user = {
...records.messages[0]!,
id: MessageId.make("discarded-request"),
text: "An abandoned topic about cooking.",
};
records.runs.push({
id: runId,
threadId,
ordinal: 1,
providerInstanceId: records.thread.providerInstanceId,
modelSelection: records.thread.modelSelection,
providerThreadId: null,
userMessageId: user.id,
rootNodeId: null,
activeAttemptId: null,
status: scenario === "rolled-back" ? "rolled_back" : "cancelled",
requestedAt: records.thread.createdAt,
startedAt: scenario === "cancelled-before-start" ? null : records.thread.createdAt,
completedAt: records.thread.createdAt,
checkpointId: null,
contextHandoffId: null,
});
records.messages.push(user);
if (scenario !== "cancelled-before-start")
records.messages.push({
...records.messages[1]!,
id: MessageId.make("discarded-reply"),
runId,
text: "Here is a recipe.",
});
let state: unknown;
const client = HttpClient.make((request) =>
Effect.sync(() => {
assert.equal(request.body._tag, "Uint8Array");
if (request.body._tag === "Uint8Array")
state = decodeRequest(new TextDecoder().decode(request.body.body)).state;
return HttpClientResponse.fromWeb(
request,
Response.json({
model: "pplx-decider-flash-v1.2",
answers: { q: { type: "noul", noul: 0.98 } },
}),
);
}),
);
const layer = Suggestions.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(Orchestrator.OrchestratorV2)({
getMessageCount: () => Effect.succeed(records.messages.length),
getThreadRecords: <K extends ProjectionRecordField>() =>
Effect.succeed(records as unknown as ProjectionRecords<K>),
}),
ServerSettings.layerTest({
threadSplitSuggestionsEnabled: true,
threadSplitModelEndpoint: "http://127.0.0.1:8000/v1/systemone",
}),
Layer.succeed(HttpClient.HttpClient, client),
),
),
);
const result = yield* Effect.gen(function* () {
return yield* (yield* Suggestions.ThreadSplitSuggestions).suggest(input);
}).pipe(Effect.provide(layer));
assert.deepEqual(result, { suggest: true }, scenario);
assert.deepEqual(
state,
{
prior_thread: [
{ user: "Run the tests.", assistant_final: "The parser tests passed." },
...(scenario === "cancelled-after-start"
? [{ user: user.text, assistant_final: "Here is a recipe." }]
: []),
],
new_message: input.message,
},
scenario,
);
}
}),
);
11 changes: 11 additions & 0 deletions apps/server/src/orchestration-v2/ThreadSplitSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,19 @@ const make = Effect.gen(function* () {
source.thread.deletedAt
)
return noSuggestion;
const hiddenRuns = source.runs.filter(
(run) =>
run.status === "rolled_back" || (run.status === "cancelled" && run.startedAt === null),
);
const hiddenRunIds = new Set(hiddenRuns.map((run) => run.id));
const hiddenMessageIds = new Set(hiddenRuns.map((run) => run.userMessageId));
const prior: Array<{ user: string; assistant_final: string }> = [];
for (const message of source.messages) {
if (
(message.runId !== null && hiddenRunIds.has(message.runId)) ||
hiddenMessageIds.has(message.id)
)
continue;
if (
message.streaming ||
message.attachments.length ||
Expand Down
Loading
Loading