1import * as Schema from "effect/Schema";
2import { IsoDateTime, NonNegativeInt, ProjectId, TrimmedNonEmptyString } from "./baseSchemas.ts";
3import { ProviderInstanceId } from "./providerInstance.ts";
4
5/** Coding agent home directories the scanner knows how to read. */
6export const AgentSessionSource = Schema.Literals(["claudeAgent", "codex"]);
7export type AgentSessionSource = typeof AgentSessionSource.Type;
8
9/** File identity saved with an imported session so bounded retries can skip unchanged history. */
10export const AgentSessionImportSource = Schema.Struct({
11 provider: AgentSessionSource,
12 providerInstanceId: ProviderInstanceId,
13 providerSessionId: TrimmedNonEmptyString,
14 filePath: TrimmedNonEmptyString,
15 size: NonNegativeInt,
16 mtimeMs: Schema.NullOr(Schema.Number),
17 device: Schema.Number,
18 inode: Schema.NullOr(Schema.Number),
19 birthtimeMs: Schema.NullOr(Schema.Number),
20});
21export type AgentSessionImportSource = typeof AgentSessionImportSource.Type;
22
23/**
24 * Empty for now. Kept as a struct so future scan options (source filters,
25 * explicit roots) can be added without a new method.
26 */
27export const AgentSessionScanInput = Schema.Struct({});
28export type AgentSessionScanInput = typeof AgentSessionScanInput.Type;
29
30/**
31 * A directory that at least one agent CLI has run in, suitable for import as a
32 * T3 Code project. `alreadyImported` marks candidates that already have an
33 * active project rooted at the same path.
34 */
35/**
36 * Git identity of a candidate directory, read from `.git/config` without
37 * spawning git. `remoteKey` is the normalized origin URL, shared by every
38 * clone of the same repository so the client can group them. `repository`
39 * is the GitHub `owner/name` when the origin is on GitHub.
40 */
41export const AgentSessionProjectGit = Schema.Struct({
42 remoteKey: Schema.NullOr(Schema.String),
43 repository: Schema.NullOr(Schema.String),
44});
45export type AgentSessionProjectGit = typeof AgentSessionProjectGit.Type;
46
47export const AgentSessionProjectCandidate = Schema.Struct({
48 path: TrimmedNonEmptyString,
49 title: TrimmedNonEmptyString,
50 projectId: Schema.optional(ProjectId),
51 sources: Schema.Array(AgentSessionSource),
52 threadCount: NonNegativeInt,
53 lastActiveAt: Schema.NullOr(IsoDateTime),
54 alreadyImported: Schema.Boolean,
55 /**
56 * `null` when the directory is not the root of a git repository. Missing on
57 * servers that predate the git scan, where the client cannot tell repositories
58 * from plain folders and should treat every candidate as a standalone project.
59 */
60 git: Schema.optionalKey(Schema.NullOr(AgentSessionProjectGit)),
61});
62export type AgentSessionProjectCandidate = typeof AgentSessionProjectCandidate.Type;
63
64export const AgentSessionScanResult = Schema.Struct({
65 candidates: Schema.Array(AgentSessionProjectCandidate),
66 scannedAt: IsoDateTime,
67 truncated: Schema.optional(Schema.Boolean),
68});
69export type AgentSessionScanResult = typeof AgentSessionScanResult.Type;
70
71export const AgentSessionImportInput = Schema.Struct({
72 projectId: ProjectId,
73 expectedWorkspaceRoot: Schema.optional(TrimmedNonEmptyString),
74});
75export type AgentSessionImportInput = typeof AgentSessionImportInput.Type;
76
77export class AgentSessionImportProjectNotFoundError extends Schema.TaggedError<AgentSessionImportProjectNotFoundError>()(
78 "AgentSessionImportProjectNotFoundError",
79 { projectId: ProjectId },
80) {
81 override get message(): string {
82 return `Project '${this.projectId}' does not exist.`;
83 }
84}
85
86export class AgentSessionImportProjectChangedError extends Schema.TaggedError<AgentSessionImportProjectChangedError>()(
87 "AgentSessionImportProjectChangedError",
88 { projectId: ProjectId },
89) {
90 override get message(): string {
91 return `Project '${this.projectId}' changed directories. Scan for projects again before importing history.`;
92 }
93}
94
95export const AgentSessionImportResult = Schema.Struct({
96 importedCount: NonNegativeInt,
97 skippedCount: NonNegativeInt,
98});
99export type AgentSessionImportResult = typeof AgentSessionImportResult.Type;
100
101export class AgentSessionScanError extends Schema.TaggedError<AgentSessionScanError>()(
102 "AgentSessionScanError",
103 {
104 operation: Schema.Literals(["read-settings", "read-projects"]),
105 cause: Schema.Defect(),
106 },
107) {
108 override get message(): string {
109 return `Failed to scan agent sessions during ${this.operation}.`;
110 }
111}