Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
10 changes: 10 additions & 0 deletions apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,15 @@ describe("CheckpointDiffQueryLive", () => {
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getProjectShellById: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
}),
),
);
Expand Down Expand Up @@ -136,10 +141,15 @@ describe("CheckpointDiffQueryLive", () => {
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
getShellSnapshot: () =>
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getProjectShellById: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
}),
),
);
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,20 @@ describe("OrchestrationEngine", () => {
Layer.provide(
Layer.succeed(ProjectionSnapshotQuery, {
getSnapshot: () => Effect.succeed(projectionSnapshot),
getShellSnapshot: () =>
Effect.succeed({
snapshotSequence: projectionSnapshot.snapshotSequence,
projects: [],
threads: [],
updatedAt: projectionSnapshot.updatedAt,
}),
getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
getProjectShellById: () => Effect.succeed(Option.none()),
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
}),
),
Layer.provide(
Expand Down
120 changes: 119 additions & 1 deletion apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ApprovalRequestId,
type ChatAttachment,
type OrchestrationEvent,
ThreadId,
} from "@t3tools/contracts";
import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";
Expand Down Expand Up @@ -89,6 +90,71 @@
return typeof requestId === "string" ? ApprovalRequestId.make(requestId) : null;
}

function derivePendingUserInputCountFromActivities(
activities: ReadonlyArray<ProjectionThreadActivity>,
): number {
const openRequestIds = new Set<string>();
const ordered = [...activities].toSorted(
(left, right) =>
left.createdAt.localeCompare(right.createdAt) ||
left.activityId.localeCompare(right.activityId),
);

for (const activity of ordered) {
const requestId = extractActivityRequestId(activity.payload);
if (requestId === null) {
continue;
}
const payload =
typeof activity.payload === "object" && activity.payload !== null
? (activity.payload as Record<string, unknown>)
: null;
const detail = typeof payload?.detail === "string" ? payload.detail.toLowerCase() : null;

if (activity.kind === "user-input.requested") {
openRequestIds.add(requestId);
continue;
}

if (activity.kind === "user-input.resolved") {
openRequestIds.delete(requestId);
continue;
}

if (
activity.kind === "provider.user-input.respond.failed" &&
detail !== null &&
(detail.includes("stale pending user-input request") ||
detail.includes("unknown pending user-input request"))
) {
openRequestIds.delete(requestId);
}
}

return openRequestIds.size;
}

function deriveHasActionableProposedPlan(input: {
readonly latestTurnId: string | null;
readonly proposedPlans: ReadonlyArray<ProjectionThreadProposedPlan>;
}): boolean {
const sorted = [...input.proposedPlans].toSorted(
(left, right) =>
left.updatedAt.localeCompare(right.updatedAt) || left.planId.localeCompare(right.planId),
);

const latestForTurn =
input.latestTurnId === null
? null
: (sorted.filter((plan) => plan.turnId === input.latestTurnId).at(-1) ?? null);

Check warning on line 149 in apps/server/src/orchestration/Layers/ProjectionPipeline.ts

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-unicorn(prefer-array-find)

Prefer `find` over filtering and accessing the first result.
if (latestForTurn !== null) {
return latestForTurn.implementedAt === null;
}

const latestPlan = sorted.at(-1) ?? null;
return latestPlan !== null && latestPlan.implementedAt === null;
}

function retainProjectionMessagesAfterRevert(
messages: ReadonlyArray<ProjectionThreadMessage>,
turns: ReadonlyArray<ProjectionTurn>,
Expand Down Expand Up @@ -432,6 +498,48 @@
}
});

const refreshThreadShellSummary = Effect.fn("refreshThreadShellSummary")(function* (
threadId: ThreadId,
) {
const existingRow = yield* projectionThreadRepository.getById({
threadId,
});
if (Option.isNone(existingRow)) {
return;
}

const [messages, proposedPlans, activities, pendingApprovals] = yield* Effect.all([
projectionThreadMessageRepository.listByThreadId({ threadId }),
projectionThreadProposedPlanRepository.listByThreadId({ threadId }),
projectionThreadActivityRepository.listByThreadId({ threadId }),
projectionPendingApprovalRepository.listByThreadId({ threadId }),
]);

const latestUserMessageAt =
messages
.filter((message) => message.role === "user")
.map((message) => message.createdAt)
.toSorted()
.at(-1) ?? null;

const pendingApprovalCount = pendingApprovals.filter(
(approval) => approval.status === "pending",
).length;
const pendingUserInputCount = derivePendingUserInputCountFromActivities(activities);
const hasActionableProposedPlan = deriveHasActionableProposedPlan({
latestTurnId: existingRow.value.latestTurnId,
proposedPlans,
});

yield* projectionThreadRepository.upsert({
...existingRow.value,
latestUserMessageAt,
pendingApprovalCount,
pendingUserInputCount,
hasActionableProposedPlan: hasActionableProposedPlan ? 1 : 0,
});
});

const applyThreadsProjection: ProjectorDefinition["apply"] = Effect.fn(
"applyThreadsProjection",
)(function* (event, attachmentSideEffects) {
Expand All @@ -450,6 +558,10 @@
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
archivedAt: null,
latestUserMessageAt: null,
pendingApprovalCount: 0,
pendingUserInputCount: 0,
hasActionableProposedPlan: 0,
deletedAt: null,
});
return;
Expand Down Expand Up @@ -554,7 +666,9 @@

case "thread.message-sent":
case "thread.proposed-plan-upserted":
case "thread.activity-appended": {
case "thread.activity-appended":
case "thread.approval-response-requested":
case "thread.user-input-response-requested": {
const existingRow = yield* projectionThreadRepository.getById({
threadId: event.payload.threadId,
});
Expand All @@ -565,6 +679,7 @@
...existingRow.value,
updatedAt: event.occurredAt,
});
yield* refreshThreadShellSummary(event.payload.threadId);
Comment thread
juliusmarminge marked this conversation as resolved.
return;
}

Expand All @@ -580,6 +695,7 @@
latestTurnId: event.payload.session.activeTurnId,
updatedAt: event.occurredAt,
});
yield* refreshThreadShellSummary(event.payload.threadId);
return;
}

Expand All @@ -595,6 +711,7 @@
latestTurnId: event.payload.turnId,
updatedAt: event.occurredAt,
});
yield* refreshThreadShellSummary(event.payload.threadId);
return;
}

Expand All @@ -610,6 +727,7 @@
latestTurnId: null,
updatedAt: event.occurredAt,
});
yield* refreshThreadShellSummary(event.payload.threadId);
return;
}

Expand Down
Loading
Loading