Skip to content
Closed
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 @@ -59,6 +59,7 @@ function makeSnapshot(input: {
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
deletedAt: null,
archivedAt: null,
messages: [],
activities: [],
proposedPlans: [],
Expand Down
31 changes: 31 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
deletedAt: null,
archivedAt: null,
});
return;

Expand Down Expand Up @@ -494,6 +495,36 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
return;
}

case "thread.archived": {
const existingRow = yield* projectionThreadRepository.getById({
threadId: event.payload.threadId,
});
if (Option.isNone(existingRow)) {
return;
}
yield* projectionThreadRepository.upsert({
...existingRow.value,
archivedAt: event.payload.archivedAt,
updatedAt: event.payload.archivedAt,
});
return;
}

case "thread.unarchived": {
const existingRow = yield* projectionThreadRepository.getById({
threadId: event.payload.threadId,
});
if (Option.isNone(existingRow)) {
return;
}
yield* projectionThreadRepository.upsert({
...existingRow.value,
archivedAt: null,
updatedAt: event.payload.unarchivedAt,
});
return;
}

case "thread.message-sent":
case "thread.proposed-plan-upserted":
case "thread.activity-appended": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
createdAt: "2026-02-24T00:00:02.000Z",
updatedAt: "2026-02-24T00:00:03.000Z",
deletedAt: null,
archivedAt: null,
messages: [
{
id: asMessageId("message-1"),
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ThreadRuntimeModeSetPayload as ContractsThreadRuntimeModeSetPayloadSchema,
ThreadInteractionModeSetPayload as ContractsThreadInteractionModeSetPayloadSchema,
ThreadDeletedPayload as ContractsThreadDeletedPayloadSchema,
ThreadArchivedPayload as ContractsThreadArchivedPayloadSchema,
ThreadUnarchivedPayload as ContractsThreadUnarchivedPayloadSchema,
ThreadMessageSentPayload as ContractsThreadMessageSentPayloadSchema,
ThreadProposedPlanUpsertedPayload as ContractsThreadProposedPlanUpsertedPayloadSchema,
ThreadSessionSetPayload as ContractsThreadSessionSetPayloadSchema,
Expand All @@ -30,6 +32,8 @@ export const ThreadMetaUpdatedPayload = ContractsThreadMetaUpdatedPayloadSchema;
export const ThreadRuntimeModeSetPayload = ContractsThreadRuntimeModeSetPayloadSchema;
export const ThreadInteractionModeSetPayload = ContractsThreadInteractionModeSetPayloadSchema;
export const ThreadDeletedPayload = ContractsThreadDeletedPayloadSchema;
export const ThreadArchivedPayload = ContractsThreadArchivedPayloadSchema;
export const ThreadUnarchivedPayload = ContractsThreadUnarchivedPayloadSchema;

export const MessageSentPayloadSchema = ContractsThreadMessageSentPayloadSchema;
export const ThreadProposedPlanUpsertedPayload = ContractsThreadProposedPlanUpsertedPayloadSchema;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration/commandInvariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const readModel: OrchestrationReadModel = {
proposedPlans: [],
checkpoints: [],
deletedAt: null,
archivedAt: null,
},
{
id: ThreadId.makeUnsafe("thread-2"),
Expand All @@ -83,6 +84,7 @@ const readModel: OrchestrationReadModel = {
proposedPlans: [],
checkpoints: [],
deletedAt: null,
archivedAt: null,
},
],
};
Expand Down
44 changes: 44 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,50 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
};
}

case "thread.archive": {
yield* requireThread({
readModel,
command,
threadId: command.threadId,
});
const occurredAt = nowIso();
return {
...withEventBase({
aggregateKind: "thread",
aggregateId: command.threadId,
occurredAt,
commandId: command.commandId,
}),
type: "thread.archived",
payload: {
threadId: command.threadId,
archivedAt: occurredAt,
},
};
}

case "thread.unarchive": {
yield* requireThread({
readModel,
command,
threadId: command.threadId,
});
const occurredAt = nowIso();
return {
...withEventBase({
aggregateKind: "thread",
aggregateId: command.threadId,
occurredAt,
commandId: command.commandId,
}),
type: "thread.unarchived",
payload: {
threadId: command.threadId,
unarchivedAt: occurredAt,
},
};
}

case "thread.meta.update": {
yield* requireThread({
readModel,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/orchestration/projector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("orchestration projector", () => {
createdAt: now,
updatedAt: now,
deletedAt: null,
archivedAt: null,
messages: [],
proposedPlans: [],
activities: [],
Expand Down
25 changes: 25 additions & 0 deletions apps/server/src/orchestration/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
ThreadActivityAppendedPayload,
ThreadCreatedPayload,
ThreadDeletedPayload,
ThreadArchivedPayload,
ThreadUnarchivedPayload,
ThreadInteractionModeSetPayload,
ThreadMetaUpdatedPayload,
ThreadProposedPlanUpsertedPayload,
Expand Down Expand Up @@ -261,6 +263,7 @@ export function projectEvent(
createdAt: payload.createdAt,
updatedAt: payload.updatedAt,
deletedAt: null,
archivedAt: null,
messages: [],
activities: [],
checkpoints: [],
Expand Down Expand Up @@ -289,6 +292,28 @@ export function projectEvent(
})),
);

case "thread.archived":
return decodeForEvent(ThreadArchivedPayload, event.payload, event.type, "payload").pipe(
Effect.map((payload) => ({
...nextBase,
threads: updateThread(nextBase.threads, payload.threadId, {
archivedAt: payload.archivedAt,
updatedAt: payload.archivedAt,
}),
})),
);

case "thread.unarchived":
return decodeForEvent(ThreadUnarchivedPayload, event.payload, event.type, "payload").pipe(
Effect.map((payload) => ({
...nextBase,
threads: updateThread(nextBase.threads, payload.threadId, {
archivedAt: null,
updatedAt: payload.unarchivedAt,
}),
})),
);

case "thread.meta-updated":
return decodeForEvent(ThreadMetaUpdatedPayload, event.payload, event.type, "payload").pipe(
Effect.map((payload) => ({
Expand Down
15 changes: 10 additions & 5 deletions apps/server/src/persistence/Layers/ProjectionThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
latest_turn_id,
created_at,
updated_at,
deleted_at
deleted_at,
archived_at
)
VALUES (
${row.threadId},
Expand All @@ -45,7 +46,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
${row.latestTurnId},
${row.createdAt},
${row.updatedAt},
${row.deletedAt}
${row.deletedAt},
${row.archivedAt}
)
ON CONFLICT (thread_id)
DO UPDATE SET
Expand All @@ -59,7 +61,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
latest_turn_id = excluded.latest_turn_id,
created_at = excluded.created_at,
updated_at = excluded.updated_at,
deleted_at = excluded.deleted_at
deleted_at = excluded.deleted_at,
archived_at = excluded.archived_at
`,
});

Expand All @@ -80,7 +83,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
deleted_at AS "deletedAt"
deleted_at AS "deletedAt",
archived_at AS "archivedAt"
FROM projection_threads
WHERE thread_id = ${threadId}
`,
Expand All @@ -103,7 +107,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
deleted_at AS "deletedAt"
deleted_at AS "deletedAt",
archived_at AS "archivedAt"
FROM projection_threads
WHERE project_id = ${projectId}
ORDER BY created_at ASC, thread_id ASC
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplemen
import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts";
import Migration0016 from "./Migrations/016_ProjectionThreadsInteractionModeChatCodePlan.ts";
import Migration0017 from "./Migrations/017_EnvironmentVariables.ts";
import Migration0018 from "./Migrations/018_ProjectionThreadsArchivedAt.ts";
import { Effect } from "effect";

/**
Expand Down Expand Up @@ -59,6 +60,7 @@ const loader = Migrator.fromRecord({
"15_ProjectionTurnsSourceProposedPlan": Migration0015,
"16_ProjectionThreadsInteractionModeChatCodePlan": Migration0016,
"17_EnvironmentVariables": Migration0017,
"18_ProjectionThreadsArchivedAt": Migration0018,
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as Effect from "effect/Effect";

export default Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

yield* sql`
ALTER TABLE projection_threads
ADD COLUMN archived_at TEXT
`;
});
1 change: 1 addition & 0 deletions apps/server/src/persistence/Services/ProjectionThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ProjectionThread = Schema.Struct({
createdAt: IsoDateTime,
updatedAt: IsoDateTime,
deletedAt: Schema.NullOr(IsoDateTime),
archivedAt: Schema.NullOr(IsoDateTime),
});
export type ProjectionThread = typeof ProjectionThread.Type;

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const AppSettingsSchema = Schema.Struct({
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "worktree" as const satisfies EnvMode)),
confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
showArchivedThreads: Schema.Boolean.pipe(withDefaults(() => false)),
diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)),
enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)),
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ function createSnapshotForTargetUser(options: {
createdAt: NOW_ISO,
updatedAt: NOW_ISO,
deletedAt: null,
archivedAt: null,
messages,
activities: [],
proposedPlans: [],
Expand Down Expand Up @@ -296,6 +297,7 @@ function addThreadToSnapshot(
createdAt: NOW_ISO,
updatedAt: NOW_ISO,
deletedAt: null,
archivedAt: null,
messages: [],
activities: [],
proposedPlans: [],
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/KeybindingsToast.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function createMinimalSnapshot(): OrchestrationReadModel {
createdAt: NOW_ISO,
updatedAt: NOW_ISO,
deletedAt: null,
archivedAt: null,
messages: [
{
id: "msg-1" as MessageId,
Expand Down
Loading
Loading