1import {
2 type OrchestrationV2ThreadProjection,
3 type OrchestrationV2ThreadShell,
4 ProjectId,
5 type ProviderInteractionMode,
6 ProviderInstanceId,
7 RunId,
8 type RuntimeMode,
9 type ThreadId,
10} from "@t3tools/contracts";
11import * as DateTime from "effect/DateTime";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14
15import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
16
17const EPOCH = DateTime.makeUnsafe("2026-01-01T00:00:00.000Z");
18
19/**
20 * A thread in the middle of a turn on the `codex` instance: the shape the
21 * access gate reads for a thread caller, and for the threads a caller targets.
22 */
23export const liveThreadShell = (
24 id: ThreadId,
25 modes: {
26 readonly runtimeMode?: RuntimeMode;
27 readonly interactionMode?: ProviderInteractionMode;
28 readonly activeRunId?: RunId | null;
29 } = {},
30): OrchestrationV2ThreadShell => {
31 const providerInstanceId = ProviderInstanceId.make("codex");
32 return {
33 id,
34 projectId: ProjectId.make("project:mcp-test"),
35 title: id,
36 providerInstanceId,
37 modelSelection: { instanceId: providerInstanceId, model: "gpt-5" },
38 runtimeMode: modes.runtimeMode ?? "full-access",
39 interactionMode: modes.interactionMode ?? "default",
40 branch: null,
41 worktreePath: null,
42 createdBy: "user",
43 creationSource: "web",
44 activeProviderThreadId: null,
45 lineage: { rootThreadId: id, parentThreadId: null, relationshipToParent: null },
46 forkedFrom: null,
47 latestRunId: null,
48 activeRunId: modes.activeRunId === undefined ? RunId.make("run:mcp-test") : modes.activeRunId,
49 status: "running",
50 pendingRuntimeRequest: null,
51 latestVisibleMessage: null,
52 latestUserMessageAt: null,
53 hasActionableProposedPlan: false,
54 itemCount: 0,
55 visibleItemCount: 0,
56 createdAt: EPOCH,
57 updatedAt: EPOCH,
58 archivedAt: null,
59 settledOverride: null,
60 settledAt: null,
61 lastVisitedAt: null,
62 deletedAt: null,
63 };
64};
65
66/**
67 * `ThreadManagementService` that answers only the access gate's lookups,
68 * every thread a live full-access one. For tests of a tool rather than of who
69 * may call it.
70 */
71export const liveThreadsLayer = Layer.mock(ThreadManagement.ThreadManagementService)({
72 getThreadShell: (threadId) => Effect.succeed(liveThreadShell(threadId)),
73});
74
75/**
76 * The thread of `shell` with no runs, messages or other records: what a
77 * caller looks like once its turn has ended.
78 */
79export const idleThreadProjection = (
80 shell: OrchestrationV2ThreadShell,
81): OrchestrationV2ThreadProjection => {
82 const {
83 status: _status,
84 activeRunId: _activeRunId,
85 latestRunId: _latestRunId,
86 pendingRuntimeRequest: _pendingRuntimeRequest,
87 latestVisibleMessage: _latestVisibleMessage,
88 latestUserMessageAt: _latestUserMessageAt,
89 hasActionableProposedPlan: _hasActionableProposedPlan,
90 itemCount: _itemCount,
91 visibleItemCount: _visibleItemCount,
92 lastVisitedAt,
93 ...thread
94 } = shell;
95 return {
96 thread: { ...thread, lastVisitedAt: lastVisitedAt ?? null },
97 runs: [],
98 attempts: [],
99 nodes: [],
100 subagents: [],
101 providerSessions: [],
102 providerThreads: [],
103 providerTurns: [],
104 runtimeRequests: [],
105 messages: [],
106 plans: [],
107 turnItems: [],
108 checkpointScopes: [],
109 checkpoints: [],
110 contextHandoffs: [],
111 contextTransfers: [],
112 visibleTurnItems: [],
113 updatedAt: shell.updatedAt,
114 };
115};