diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts index a34f8db9028..ca45eb3aa66 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts @@ -84,6 +84,75 @@ it("includes imported runless history when selecting fork context through a run" }); it.layer(TestLayer)("ProjectionStoreV2", (it) => { + it.effect("does not treat visited or marked-unread state as thread activity", () => + Effect.gen(function* () { + const projectionStore = yield* ProjectionStoreV2; + const createdAt = yield* DateTime.now; + const visitedOccurredAt = DateTime.add(createdAt, { seconds: 1 }); + const markedUnreadOccurredAt = DateTime.add(createdAt, { seconds: 2 }); + const threadId = ThreadId.make("thread:projection-read-state"); + const projectId = ProjectId.make("project:projection-read-state"); + const thread = { + createdBy: "user" as const, + creationSource: "web" as const, + id: threadId, + projectId, + title: "Projection read state", + providerInstanceId, + modelSelection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + branch: null, + worktreePath: null, + activeProviderThreadId: null, + lineage: { + parentThreadId: null, + relationshipToParent: null, + rootThreadId: threadId, + }, + forkedFrom: null, + createdAt, + updatedAt: createdAt, + archivedAt: null, + settledOverride: null, + settledAt: null, + lastVisitedAt: null, + deletedAt: null, + }; + + yield* projectionStore.apply({ + id: EventId.make("event:projection-read-state:created"), + type: "thread.created", + threadId, + occurredAt: createdAt, + payload: thread, + }); + yield* projectionStore.apply({ + id: EventId.make("event:projection-read-state:visited"), + type: "thread.visited", + threadId, + occurredAt: visitedOccurredAt, + payload: { ...thread, lastVisitedAt: createdAt }, + }); + + const visited = yield* projectionStore.getThreadProjection(threadId); + assert.deepEqual(visited.thread.lastVisitedAt, createdAt); + assert.deepEqual(visited.thread.updatedAt, createdAt); + + yield* projectionStore.apply({ + id: EventId.make("event:projection-read-state:marked-unread"), + type: "thread.marked-unread", + threadId, + occurredAt: markedUnreadOccurredAt, + payload: thread, + }); + + const markedUnread = yield* projectionStore.getThreadProjection(threadId); + assert.isNull(markedUnread.thread.lastVisitedAt); + assert.deepEqual(markedUnread.thread.updatedAt, createdAt); + }), + ); + it.effect("only exposes interruptible runs through the shell activeRunId", () => Effect.gen(function* () { const projectionStore = yield* ProjectionStoreV2; diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts index 8f83cf5392f..645283705d5 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -1812,6 +1812,8 @@ export const layer: Layer.Layer = event.type !== "thread.unsettled" && event.type !== "thread.snoozed" && event.type !== "thread.unsnoozed" && + event.type !== "thread.visited" && + event.type !== "thread.marked-unread" && event.type !== "thread.metadata-updated" && event.type !== "thread.runtime-mode-updated" && event.type !== "thread.interaction-mode-updated" && diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 9cf35916162..cf6559a008c 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -19,6 +19,7 @@ import * as DateTime from "effect/DateTime"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as CheckpointStore from "../checkpointing/CheckpointStore.ts"; @@ -1335,6 +1336,7 @@ it.layer(SharedApplicationDataPlaneTestLayer)("visited projection", (it) => { assert.isNull(created.thread.lastVisitedAt); const createdUpdatedAt = created.thread.updatedAt; + yield* TestClock.adjust("1 second"); yield* orchestrator.dispatch({ type: "thread.visit", commandId: CommandId.make("runtime-layer-visited-thread-visit"),