Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const layerManagement = Layer.unwrap(
.pipe(Effect.orDie),
getThreadShell: (id) => projections.getThreadShell(id).pipe(Effect.orDie),
getShellSnapshot: (options) => projections.getShellSnapshot(options).pipe(Effect.orDie),
listActiveShellIndex: () => projections.listActiveShellIndex().pipe(Effect.orDie),
listActiveSubagentThreadIds: () =>
projections.listActiveSubagentThreadIds().pipe(Effect.orDie),
readShellSnapshot: (options) =>
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ import {
type ProjectionRecords,
type ProjectionCheckpointContext,
type OlderSettledThreadsListing,
type ActiveShellIndexEntry,
type OlderSettledThreadsQuery,
type ShellSnapshotOptions,
} from "./ProjectionStore.ts";
Expand Down Expand Up @@ -392,6 +393,11 @@ export interface OrchestratorV2Shape {
ReadonlyArray<ThreadId>,
OrchestratorV2Error
>;
readonly countThreads: () => Effect.Effect<number, OrchestratorV2Error>;
readonly listActiveShellIndex: () => Effect.Effect<
ReadonlyArray<ActiveShellIndexEntry>,
OrchestratorV2Error
>;
readonly listOlderSettledThreads: (
query: OlderSettledThreadsQuery,
) => Effect.Effect<OlderSettledThreadsListing, OrchestratorV2Error>;
Expand Down Expand Up @@ -13434,6 +13440,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
.pipe(Effect.mapError(shellProjectionError));
},
),
countThreads: Effect.fn("OrchestratorV2.countThreads")(function () {
return projectionStore.countThreads().pipe(Effect.mapError(shellProjectionError));
}),
listActiveShellIndex: Effect.fn("OrchestratorV2.listActiveShellIndex")(function () {
return projectionStore.listActiveShellIndex().pipe(Effect.mapError(shellProjectionError));
}),
listOlderSettledThreads: Effect.fn("OrchestratorV2.listOlderSettledThreads")(function (query) {
return projectionStore
.listOlderSettledThreads(query)
Expand Down
34 changes: 34 additions & 0 deletions apps/server/src/orchestration-v2/ProjectionStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ThreadId,
TurnItemId,
type ThreadPullRequestLink,
isLiveSubagentShell,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -76,6 +77,33 @@ const traceSqlStatements = () => {
return { statements, tracer };
};

/** The shell index flags every live shell and matches its shells on the fields subscriber trims read. */
const assertShellIndexCoversShells = Effect.fnUntraced(function* () {
const store = yield* ProjectionStore.ProjectionStoreV2;
const shells = (yield* store.getShellSnapshot({ location: "active" })).threads;
const index = yield* store.listActiveShellIndex();
const iso = (value: DateTime.Utc | null | undefined) =>
value == null ? null : DateTime.formatIso(value);
const trimmed = (
row: Pick<
ProjectionStore.ActiveShellIndexEntry,
"id" | "projectId" | "lineage" | "settledOverride" | "settledAt" | "pinnedAt" | "snoozedUntil"
>,
) => ({
id: row.id,
projectId: row.projectId,
lineage: row.lineage,
settledOverride: row.settledOverride,
settledAt: iso(row.settledAt),
pinnedAt: iso(row.pinnedAt),
snoozedUntil: iso(row.snoozedUntil),
});
assert.deepEqual(index.map(trimmed), shells.map(trimmed));
shells.forEach((shell, position) => {
if (isLiveSubagentShell(shell)) assert.isTrue(index[position]?.mayBeLive, shell.id);
});
});

const addRolledBackRecoveryCandidate = Effect.fn("addRolledBackRecoveryCandidate")(function* (
suffix: string,
) {
Expand Down Expand Up @@ -272,6 +300,7 @@ it.effect("SQL shells flag only an active turn blocked solely on T3 thread waits
assert.equal(shell?.status, "running");
assert.equal(shell?.blockedOnThreadWait ?? false, expected);
}
yield* assertShellIndexCoversShells();
});
const wait = {
...itemBase,
Expand Down Expand Up @@ -531,6 +560,7 @@ it.effect("SQL shells use the latest subagent owner for duplicate provider-threa
const listed = (yield* store.getShellSnapshot()).threads.find(
(thread) => thread.id === targetThreadId,
);
yield* assertShellIndexCoversShells();
for (const shell of [yield* store.getThreadShell(targetThreadId), listed]) {
assert.equal(shell?.providerInstanceId, otherInstanceId);
assert.equal(shell?.activeRunId, null);
Expand Down Expand Up @@ -1336,6 +1366,7 @@ it.effect("SQL list older settled threads below a cursor", () =>
const next = yield* list({ settledAt: DateTime.formatIso(at(4)), threadId: id("b") }, 10);
assert.deepEqual(next.threadIds, [id("c"), id("d"), id("snoozed")]);
assert.deepEqual(yield* list(before, 25, []), { threadIds: [], counts: [] });
yield* assertShellIndexCoversShells();
const scoped = yield* list(before, 0, [ProjectId.make("project:b")]);
assert.deepEqual(scoped, {
threadIds: [],
Expand Down Expand Up @@ -3798,6 +3829,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
const sqlShell = (yield* projectionStore.getShellSnapshot()).threads.find(
(row) => row.id === threadId,
)!;
yield* assertShellIndexCoversShells();
for (const questionShell of [
sqlShell,
ProjectionStore.threadShellFromProjection(projection),
Expand Down Expand Up @@ -4860,6 +4892,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
SET status = 'running', payload_json = json_set(payload_json, '$.status', 'running')
WHERE run_id = ${runId}
`;
yield* assertShellIndexCoversShells();
const secretOnlyShell = (yield* projectionStore.getShellSnapshot()).threads.find(
(thread) => thread.id === threadId,
);
Expand Down Expand Up @@ -6466,6 +6499,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
(candidate) => candidate.id === threadId,
);
assert.deepEqual(listed?.pendingBackgroundTasks, shell?.pendingBackgroundTasks);
yield* assertShellIndexCoversShells();
return shell && projectThreadAwarenessV2({ environmentId, project, thread: shell })?.phase;
});

Expand Down
110 changes: 110 additions & 0 deletions apps/server/src/orchestration-v2/ProjectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,15 @@ export interface ShellSnapshotOptions {
readonly threadIds?: ReadonlyArray<ThreadId>;
}

/** What a shell subscriber's trims read from an active thread, without building its shell. */
export interface ActiveShellIndexEntry extends Pick<
OrchestrationV2ThreadShell,
"id" | "projectId" | "lineage" | "settledOverride" | "settledAt" | "pinnedAt" | "snoozedUntil"
> {
/** False only when no run, request, work item, or watch can make the shell live. */
readonly mayBeLive: boolean;
}

export interface OlderSettledThreadsQuery {
readonly before: OrchestrationV2OlderSettledCursor;
readonly projectIds?: ReadonlyArray<ProjectId> | undefined;
Expand Down Expand Up @@ -450,6 +459,13 @@ export interface ProjectionStoreV2Shape {
ReadonlyArray<ThreadId>,
ProjectionStoreV2Error
>;
/** Threads that are not deleted, archived ones included. */
readonly countThreads: () => Effect.Effect<number, ProjectionStoreV2Error>;
/** Every active thread, in shell order. */
readonly listActiveShellIndex: () => Effect.Effect<
ReadonlyArray<ActiveShellIndexEntry>,
ProjectionStoreV2Error
>;
/** Older settled thread ids below a cursor, newest settle first, and per-project counts. */
readonly listOlderSettledThreads: (
query: OlderSettledThreadsQuery,
Expand Down Expand Up @@ -6614,6 +6630,98 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
Effect.mapError(shellSnapshotReadError),
);

const countThreads = Effect.fn("ProjectionStore.countThreads")(function* () {
const rows = yield* sql<{ readonly count: number }>`
SELECT COUNT(*) AS count FROM orchestration_v2_projection_threads WHERE deleted_at IS NULL
`;
return rows[0]?.count ?? 0;
}, Effect.mapError(shellSnapshotReadError));

const listActiveShellIndex = Effect.fn("ProjectionStore.listActiveShellIndex")(function* () {
const rows = yield* sql<{
readonly thread_id: string;
readonly project_id: string;
readonly relationship_to_parent: string | null;
readonly parent_thread_id: string | null;
readonly root_thread_id: string;
readonly settled_override: string | null;
readonly settled_at: string | null;
readonly pinned_at: string | null;
readonly snoozed_until: string | null;
readonly may_be_live: number;
}>`
SELECT
t.thread_id,
t.project_id,
json_extract(t.payload_json, '$.lineage.relationshipToParent') AS relationship_to_parent,
json_extract(t.payload_json, '$.lineage.parentThreadId') AS parent_thread_id,
json_extract(t.payload_json, '$.lineage.rootThreadId') AS root_thread_id,
json_extract(t.payload_json, '$.settledOverride') AS settled_override,
json_extract(t.payload_json, '$.settledAt') AS settled_at,
json_extract(t.payload_json, '$.pinnedAt') AS pinned_at,
json_extract(t.payload_json, '$.snoozedUntil') AS snoozed_until,
-- A superset of isLiveSubagentShell: the sources of its run status, pending request,
-- and pending background work.
(
EXISTS (
SELECT 1 FROM orchestration_v2_projection_runs r
WHERE r.thread_id = t.thread_id
AND r.status IN ('queued', 'preparing', 'starting', 'running', 'waiting')
)
OR EXISTS (
SELECT 1 FROM orchestration_v2_projection_runtime_requests request
WHERE request.thread_id = t.thread_id AND request.status = 'pending'
)
-- Same predicates as the shell's pending item and link reads, for their partial indexes.
OR EXISTS (
SELECT 1 FROM orchestration_v2_projection_turn_items item
WHERE item.thread_id = t.thread_id
AND item.type IN ('command_execution', 'dynamic_tool', 'subagent')
AND item.status NOT IN ('completed', 'interrupted', 'failed', 'cancelled')
)
OR EXISTS (
SELECT 1 FROM orchestration_v2_projection_turn_items link_request
WHERE link_request.thread_id = t.thread_id
AND link_request.type = 'link_request' AND link_request.status = 'waiting'
)
OR EXISTS (
SELECT 1 FROM orchestration_v2_projection_provider_threads provider_thread
WHERE provider_thread.thread_id = t.thread_id
AND json_array_length(provider_thread.payload_json, '$.pendingBackgroundTasks') > 0
)
OR EXISTS (
SELECT 1 FROM json_each(t.payload_json, '$.pullRequests') link
WHERE json_type(link.value, '$.watch') IS NOT NULL
)
) AS may_be_live
FROM orchestration_v2_projection_threads t
WHERE t.deleted_at IS NULL
AND json_extract(t.payload_json, '$.archivedAt') IS NULL
ORDER BY t.updated_at ASC, t.thread_id ASC
`;
return rows.map((row): ActiveShellIndexEntry => ({
id: ThreadId.make(row.thread_id),
projectId: ProjectId.make(row.project_id),
lineage: {
parentThreadId:
row.parent_thread_id === null ? null : ThreadId.make(row.parent_thread_id),
relationshipToParent:
row.relationship_to_parent === "fork" || row.relationship_to_parent === "subagent"
? row.relationship_to_parent
: null,
rootThreadId: ThreadId.make(row.root_thread_id),
},
settledOverride:
row.settled_override === "settled" || row.settled_override === "active"
? row.settled_override
: null,
settledAt: row.settled_at === null ? null : DateTime.makeUnsafe(row.settled_at),
pinnedAt: row.pinned_at === null ? null : DateTime.makeUnsafe(row.pinned_at),
snoozedUntil: row.snoozed_until === null ? null : DateTime.makeUnsafe(row.snoozed_until),
mayBeLive: row.may_be_live === 1,
}));
}, Effect.mapError(shellSnapshotReadError));

const findGuestShareThreadId = Effect.fn("ProjectionStore.findGuestShareThreadId")(function* (
shareId: GuestShareId,
) {
Expand Down Expand Up @@ -6768,6 +6876,8 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
apply,
getShellSnapshot: readOnReader(getShellSnapshot),
listActiveSubagentThreadIds: readOnReader(listActiveSubagentThreadIds),
countThreads: readOnReader(countThreads),
listActiveShellIndex: readOnReader(listActiveShellIndex),
readShellSnapshot: readOnReader(readShellSnapshot),
listOlderSettledThreads: readOnReader(listOlderSettledThreads),
getThreadShell: readOnReader(getThreadShell),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ it.effect(
getOpenRemoteDelegatedTasks: Effect.die("unused getOpenRemoteDelegatedTasks"),
getShellSnapshot: () => Effect.die("unused getShellSnapshot"),
listActiveSubagentThreadIds: () => Effect.die("unused listActiveSubagentThreadIds"),
countThreads: () => Effect.die("unused countThreads"),
listActiveShellIndex: () => Effect.die("unused listActiveShellIndex"),
listOlderSettledThreads: () => Effect.die("unused listOlderSettledThreads"),
readShellSnapshot: () => Effect.die("unused readShellSnapshot"),
getThreadShell: () => Effect.die("unused getThreadShell"),
Expand Down
Loading
Loading