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
108 changes: 105 additions & 3 deletions apps/server/src/orchestration-v2/ThreadSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { assert, it } from "@effect/vitest";
import {
EventId,
MessageId,
NodeId,
ProjectId,
ProviderInstanceId,
RunId,
ThreadId,
TurnItemId,
type OrchestrationV2DomainEvent,
type ThreadLinkedPullRequest,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -127,6 +130,7 @@ const message = (
readonly minute?: number;
readonly streaming?: boolean;
readonly createdBy?: "user" | "agent";
readonly runId?: RunId;
} = {},
): OrchestrationV2DomainEvent => ({
id: EventId.make(`message:${id}`),
Expand All @@ -139,7 +143,7 @@ const message = (
creationSource: role === "user" ? "web" : "provider",
id: MessageId.make(id),
threadId,
runId: null,
runId: options.runId ?? null,
nodeId: null,
role,
text,
Expand Down Expand Up @@ -650,22 +654,120 @@ it.layer(layerTest)("ThreadSearch filters before it ranks", (it) => {
}),
);

it.effect("leaves out answers to an agent's question, which show in its card", () =>
it.effect("leaves out answers in their question card and turns the thread hides", () =>
Effect.gen(function* () {
const projections = yield* ProjectionStore.ProjectionStoreV2;
const search = yield* ThreadSearch.ThreadSearch;
const project = ProjectId.make("project:answers");
const asked = ThreadId.make("thread:answers");
const rewound = ThreadId.make("thread:rewound");
const queued = ThreadId.make("thread:queued");
const stopped = ThreadId.make("thread:stopped");
const recovered = ThreadId.make("thread:recovered");
const run = (owner: ThreadId, status: "rolled_back" | "cancelled") =>
({
id: EventId.make(`run:${owner}`),
type: "run.created",
threadId: owner,
runId: RunId.make(`run:${owner}`),
nodeId: NodeId.make(`node:${owner}`),
providerInstanceId,
occurredAt: at(2),
payload: {
id: RunId.make(`run:${owner}`),
threadId: owner,
ordinal: 1,
providerInstanceId,
modelSelection: { instanceId: providerInstanceId, model: "gpt-5" },
providerThreadId: null,
userMessageId: MessageId.make(`${owner}:prompt`),
rootNodeId: NodeId.make(`node:${owner}`),
activeAttemptId: null,
status,
requestedAt: at(1),
startedAt: at(1),
completedAt: at(2),
checkpointId: null,
contextHandoffId: null,
},
}) satisfies OrchestrationV2DomainEvent;
const shown = (owner: ThreadId, id: string, as: "turn_start" | "queued_turn" | "reply") =>
({
id: EventId.make(`item:${id}`),
type: "turn-item.updated",
threadId: owner,
occurredAt: at(2),
payload: {
id: TurnItemId.make(id),
threadId: owner,
runId: RunId.make(`run:${owner}`),
nodeId: null,
providerThreadId: null,
providerTurnId: null,
nativeItemRef: null,
parentItemId: null,
ordinal: 1,
status: "completed",
title: null,
startedAt: at(1),
completedAt: at(1),
updatedAt: at(1),
messageId: MessageId.make(id),
text: "",
...(as === "reply"
? { type: "assistant_message", streaming: false }
: {
type: "user_message",
inputIntent: as,
attachments: [],
createdBy: "user",
creationSource: "web",
}),
},
}) satisfies OrchestrationV2DomainEvent;
yield* createProject(project);
yield* Effect.forEach(
[
thread(asked, project),
message(asked, "async-answer:request-1", "user", "use the albatross drive"),
thread(rewound, project),
run(rewound, "rolled_back"),
message(rewound, "rewound-reply", "assistant", "the albatross drive is ready", {
runId: RunId.make(`run:${rewound}`),
}),
thread(queued, project),
run(queued, "cancelled"),
message(queued, "queued-prompt", "user", "then mount the albatross drive", {
runId: RunId.make(`run:${queued}`),
}),
// A queued prompt usually has no item; an edited one has a queued item.
message(queued, "requeued-prompt", "user", "then eject the albatross drive", {
runId: RunId.make(`run:${queued}`),
}),
shown(queued, "requeued-prompt", "queued_turn"),
// A stopped turn keeps its prompt and whatever the agent already said.
thread(stopped, project),
run(stopped, "cancelled"),
message(stopped, "stopped-prompt", "user", "format the albatross drive", {
runId: RunId.make(`run:${stopped}`),
}),
shown(stopped, "stopped-prompt", "turn_start"),
thread(recovered, project),
run(recovered, "cancelled"),
message(recovered, "recovered-reply", "assistant", "the albatross drive is formatted", {
runId: RunId.make(`run:${recovered}`),
}),
shown(recovered, "recovered-reply", "reply"),
],
projections.apply,
{ discard: true },
);
assert.deepEqual((yield* search.search({ query: "albatross" })).matches, []);
assert.deepEqual(
(yield* search.search({ query: "albatross" })).matches
.map((match) => match.messageId)
.sort(),
[MessageId.make("recovered-reply"), MessageId.make("stopped-prompt")],
);
}),
);

Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/orchestration-v2/ThreadSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,24 @@ export const messageHitsQuery = (
-- Answers to an agent's question show inside its question card, where
-- neither find nor a jump can reach them.
AND messages.message_id NOT LIKE 'async-answer:%'
-- Nor can they reach a rolled-back turn, or a message of a stopped turn
-- that no shown item carries, such as a queued prompt that never ran.
AND NOT EXISTS (
SELECT 1 FROM orchestration_v2_projection_runs AS runs
WHERE runs.run_id = messages.run_id
AND (
runs.status = 'rolled_back'
OR (
runs.status = 'cancelled'
AND NOT EXISTS (
Comment thread
saphid marked this conversation as resolved.
Resolved
Comment thread
saphid marked this conversation as resolved.
Resolved
SELECT 1 FROM orchestration_v2_projection_turn_items AS items
WHERE items.run_id = runs.run_id
AND json_extract(items.payload_json, '$.messageId') = messages.message_id
AND json_extract(items.payload_json, '$.inputIntent') IS NOT 'queued_turn'
)
)
)
)
AND NOT EXISTS (
SELECT 1 FROM json_each(${substrings}) AS token
WHERE instr(lower(json_extract(messages.payload_json, '$.text')), token.value) = 0
Expand Down
3 changes: 2 additions & 1 deletion packages/client-runtime/src/state/olderSettledThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ export function createOlderSettledThreadAtoms<R, ER, E>(
);
const searchReadAtom = createEnvironmentQueryAtomFamily(runtime, {
label: "environment-data:orchestration:older-settled-search",
idleTtlMs: 60_000,
staleTimeMs: 0,
idleTtlMs: 0,
execute: Effect.fn("OlderSettledThreads.search")(function* (input: {
readonly generation: number;
readonly search: OrchestrationV2SearchThreadsInput;
Expand Down
4 changes: 2 additions & 2 deletions packages/client-runtime/src/state/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ export function createOrchestrationEnvironmentAtoms<R, E>(
threadSearch: createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:orchestration:thread-search",
tag: ORCHESTRATION_V2_WS_METHODS.searchThreads,
staleTimeMs: 30_000,
idleTtlMs: 60_000,
staleTimeMs: 0,
idleTtlMs: 0,
}),
archivedShellSnapshot: createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:orchestration:archived-shell-snapshot",
Expand Down
Loading