@tracemarketplace/cli 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +2 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +2 -2
- package/dist/api-client.js.map +1 -1
- package/dist/cli.js +45 -14
- package/dist/cli.js.map +1 -1
- package/dist/commands/auto-submit.d.ts +2 -1
- package/dist/commands/auto-submit.d.ts.map +1 -1
- package/dist/commands/auto-submit.js +43 -56
- package/dist/commands/auto-submit.js.map +1 -1
- package/dist/commands/daemon.d.ts +8 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +118 -62
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/history.d.ts +3 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +8 -4
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/login.d.ts +5 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +25 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/register.d.ts +1 -0
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +4 -39
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/remove-hook.d.ts +6 -0
- package/dist/commands/remove-hook.d.ts.map +1 -0
- package/dist/commands/remove-hook.js +174 -0
- package/dist/commands/remove-hook.js.map +1 -0
- package/dist/commands/setup-hook.d.ts +2 -0
- package/dist/commands/setup-hook.d.ts.map +1 -1
- package/dist/commands/setup-hook.js +85 -41
- package/dist/commands/setup-hook.js.map +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +8 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +136 -83
- package/dist/commands/submit.js.map +1 -1
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +8 -4
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts +33 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +163 -17
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/flush.d.ts +46 -0
- package/dist/flush.d.ts.map +1 -0
- package/dist/flush.js +338 -0
- package/dist/flush.js.map +1 -0
- package/dist/flush.test.d.ts +2 -0
- package/dist/flush.test.d.ts.map +1 -0
- package/dist/flush.test.js +175 -0
- package/dist/flush.test.js.map +1 -0
- package/dist/submitter.d.ts.map +1 -1
- package/dist/submitter.js +5 -2
- package/dist/submitter.js.map +1 -1
- package/package.json +8 -7
- package/src/api-client.ts +3 -3
- package/src/cli.ts +51 -14
- package/src/commands/auto-submit.ts +80 -40
- package/src/commands/daemon.ts +166 -59
- package/src/commands/history.ts +9 -4
- package/src/commands/login.ts +37 -9
- package/src/commands/register.ts +5 -49
- package/src/commands/remove-hook.ts +194 -0
- package/src/commands/setup-hook.ts +93 -43
- package/src/commands/status.ts +8 -4
- package/src/commands/submit.ts +189 -83
- package/src/commands/whoami.ts +8 -4
- package/src/config.ts +223 -21
- package/src/constants.ts +18 -0
- package/src/flush.test.ts +214 -0
- package/src/flush.ts +505 -0
- package/vitest.config.ts +8 -0
- package/src/submitter.ts +0 -110
package/src/config.ts
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { DEFAULT_PROFILE } from "./constants.js";
|
|
4
5
|
|
|
5
|
-
export interface
|
|
6
|
+
export interface StoredProfileConfig {
|
|
6
7
|
apiKey: string;
|
|
7
8
|
serverUrl: string;
|
|
8
9
|
email: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
export interface Config extends StoredProfileConfig {
|
|
13
|
+
profile: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConfigStore {
|
|
17
|
+
defaultProfile: string;
|
|
18
|
+
profiles: Record<string, StoredProfileConfig>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TrackedSessionTool = "claude_code" | "codex_cli" | "cursor";
|
|
22
|
+
|
|
23
|
+
export interface SessionUploadState {
|
|
24
|
+
sourceTool: TrackedSessionTool;
|
|
25
|
+
sourceSessionId: string;
|
|
26
|
+
locator: string;
|
|
27
|
+
nextChunkIndex: number;
|
|
28
|
+
openChunkStartTurn: number;
|
|
29
|
+
lastSeenTurnCount: number;
|
|
30
|
+
lastActivityAt: string | null;
|
|
31
|
+
lastFlushedTurnId: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
export interface SubmitState {
|
|
35
|
+
version: 2;
|
|
14
36
|
chunks: Record<string, number>;
|
|
37
|
+
sessions: Record<string, SessionUploadState>;
|
|
15
38
|
}
|
|
16
39
|
|
|
40
|
+
type JsonRecord = Record<string, unknown>;
|
|
41
|
+
const EMPTY_SUBMIT_STATE: SubmitState = { version: 2, chunks: {}, sessions: {} };
|
|
42
|
+
|
|
17
43
|
export function getConfigDir(): string {
|
|
18
44
|
return join(homedir(), ".config", "tracemarketplace");
|
|
19
45
|
}
|
|
@@ -22,40 +48,216 @@ export function getConfigPath(): string {
|
|
|
22
48
|
return join(getConfigDir(), "config.json");
|
|
23
49
|
}
|
|
24
50
|
|
|
25
|
-
export function getStatePath(): string {
|
|
26
|
-
return join(getConfigDir(),
|
|
51
|
+
export function getStatePath(profile?: string): string {
|
|
52
|
+
return join(getConfigDir(), `state${profileSuffix(profile)}.json`);
|
|
27
53
|
}
|
|
28
54
|
|
|
29
|
-
export function
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
export function getAutoSubmitLogPath(profile?: string): string {
|
|
56
|
+
return join(getConfigDir(), `auto-submit${profileSuffix(profile)}.log`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getDaemonStatePath(profile?: string): string {
|
|
60
|
+
return join(getConfigDir(), `daemon-state${profileSuffix(profile)}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveProfile(profile?: string): string {
|
|
64
|
+
if (profile) return normalizeProfile(profile);
|
|
65
|
+
if (process.env.TRACEMP_PROFILE) return normalizeProfile(process.env.TRACEMP_PROFILE);
|
|
66
|
+
return normalizeProfile(loadConfigStore().defaultProfile);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadConfigStore(): ConfigStore {
|
|
70
|
+
const raw = readConfigFile();
|
|
71
|
+
if (isStoredProfileConfig(raw)) {
|
|
72
|
+
return {
|
|
73
|
+
defaultProfile: DEFAULT_PROFILE,
|
|
74
|
+
profiles: { [DEFAULT_PROFILE]: normalizeStoredProfileConfig(raw) },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isRecord(raw)) {
|
|
79
|
+
return {
|
|
80
|
+
defaultProfile: DEFAULT_PROFILE,
|
|
81
|
+
profiles: {},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const profiles = normalizeProfiles(raw.profiles);
|
|
86
|
+
const requestedDefault = normalizeProfile(
|
|
87
|
+
typeof raw.defaultProfile === "string" ? raw.defaultProfile : DEFAULT_PROFILE
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
defaultProfile: profiles[requestedDefault]
|
|
92
|
+
? requestedDefault
|
|
93
|
+
: profiles[DEFAULT_PROFILE]
|
|
94
|
+
? DEFAULT_PROFILE
|
|
95
|
+
: requestedDefault,
|
|
96
|
+
profiles,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function loadConfig(profile?: string): Config | null {
|
|
101
|
+
const store = loadConfigStore();
|
|
102
|
+
const resolvedProfile = resolveProfileFromStore(store, profile);
|
|
103
|
+
const selected = store.profiles[resolvedProfile];
|
|
104
|
+
if (!selected) return null;
|
|
105
|
+
return { profile: resolvedProfile, ...selected };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function saveConfig(
|
|
109
|
+
config: StoredProfileConfig,
|
|
110
|
+
options: { profile?: string; setDefault?: boolean } = {}
|
|
111
|
+
): Config {
|
|
112
|
+
const store = loadConfigStore();
|
|
113
|
+
const profile = normalizeProfile(options.profile);
|
|
114
|
+
|
|
115
|
+
store.profiles[profile] = normalizeStoredProfileConfig(config);
|
|
116
|
+
|
|
117
|
+
const normalizedDefault = normalizeProfile(store.defaultProfile);
|
|
118
|
+
if (options.setDefault || !store.profiles[normalizedDefault]) {
|
|
119
|
+
store.defaultProfile = profile;
|
|
120
|
+
} else {
|
|
121
|
+
store.defaultProfile = normalizedDefault;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
writeConfigStore(store);
|
|
125
|
+
return { profile, ...store.profiles[profile] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function loadState(profile?: string): SubmitState {
|
|
129
|
+
const p = getStatePath(profile);
|
|
130
|
+
if (!existsSync(p)) return EMPTY_SUBMIT_STATE;
|
|
32
131
|
try {
|
|
33
|
-
return JSON.parse(readFileSync(p, "utf-8"))
|
|
132
|
+
return normalizeSubmitState(JSON.parse(readFileSync(p, "utf-8")));
|
|
34
133
|
} catch {
|
|
35
|
-
return
|
|
134
|
+
return EMPTY_SUBMIT_STATE;
|
|
36
135
|
}
|
|
37
136
|
}
|
|
38
137
|
|
|
39
|
-
export function
|
|
138
|
+
export function saveState(state: SubmitState, profile?: string): void {
|
|
40
139
|
mkdirSync(getConfigDir(), { recursive: true });
|
|
41
|
-
writeFileSync(
|
|
140
|
+
writeFileSync(getStatePath(profile), JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function stateKey(sourceTool: string, sessionId: string): string {
|
|
144
|
+
return `${sourceTool}:${sessionId}`;
|
|
42
145
|
}
|
|
43
146
|
|
|
44
|
-
|
|
45
|
-
const p =
|
|
46
|
-
if (!existsSync(p)) return
|
|
147
|
+
function readConfigFile(): unknown {
|
|
148
|
+
const p = getConfigPath();
|
|
149
|
+
if (!existsSync(p)) return null;
|
|
47
150
|
try {
|
|
48
|
-
return JSON.parse(readFileSync(p, "utf-8"))
|
|
151
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
49
152
|
} catch {
|
|
50
|
-
return
|
|
153
|
+
return null;
|
|
51
154
|
}
|
|
52
155
|
}
|
|
53
156
|
|
|
54
|
-
|
|
157
|
+
function writeConfigStore(store: ConfigStore): void {
|
|
158
|
+
const profiles = normalizeProfiles(store.profiles);
|
|
159
|
+
const requestedDefault = normalizeProfile(store.defaultProfile);
|
|
160
|
+
const defaultProfile = profiles[requestedDefault]
|
|
161
|
+
? requestedDefault
|
|
162
|
+
: profiles[DEFAULT_PROFILE]
|
|
163
|
+
? DEFAULT_PROFILE
|
|
164
|
+
: requestedDefault;
|
|
165
|
+
|
|
55
166
|
mkdirSync(getConfigDir(), { recursive: true });
|
|
56
|
-
writeFileSync(
|
|
167
|
+
writeFileSync(
|
|
168
|
+
getConfigPath(),
|
|
169
|
+
JSON.stringify({ defaultProfile, profiles }, null, 2) + "\n",
|
|
170
|
+
"utf-8"
|
|
171
|
+
);
|
|
57
172
|
}
|
|
58
173
|
|
|
59
|
-
|
|
60
|
-
return
|
|
174
|
+
function resolveProfileFromStore(store: ConfigStore, profile?: string): string {
|
|
175
|
+
if (profile) return normalizeProfile(profile);
|
|
176
|
+
if (process.env.TRACEMP_PROFILE) return normalizeProfile(process.env.TRACEMP_PROFILE);
|
|
177
|
+
return normalizeProfile(store.defaultProfile);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function profileSuffix(profile?: string): string {
|
|
181
|
+
const resolvedProfile = normalizeProfile(profile);
|
|
182
|
+
return resolvedProfile === DEFAULT_PROFILE ? "" : `.${resolvedProfile}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeProfile(profile?: string | null): string {
|
|
186
|
+
const trimmed = (profile ?? "").trim().toLowerCase();
|
|
187
|
+
if (!trimmed) return DEFAULT_PROFILE;
|
|
188
|
+
|
|
189
|
+
const normalized = trimmed
|
|
190
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
191
|
+
.replace(/^-+/, "")
|
|
192
|
+
.replace(/-+$/, "");
|
|
193
|
+
|
|
194
|
+
return normalized || DEFAULT_PROFILE;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeProfiles(value: unknown): Record<string, StoredProfileConfig> {
|
|
198
|
+
if (!isRecord(value)) return {};
|
|
199
|
+
|
|
200
|
+
const profiles: Record<string, StoredProfileConfig> = {};
|
|
201
|
+
for (const [profile, config] of Object.entries(value)) {
|
|
202
|
+
if (!isStoredProfileConfig(config)) continue;
|
|
203
|
+
profiles[normalizeProfile(profile)] = normalizeStoredProfileConfig(config);
|
|
204
|
+
}
|
|
205
|
+
return profiles;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeStoredProfileConfig(config: StoredProfileConfig): StoredProfileConfig {
|
|
209
|
+
return {
|
|
210
|
+
apiKey: config.apiKey,
|
|
211
|
+
serverUrl: config.serverUrl,
|
|
212
|
+
email: config.email,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeSubmitState(value: unknown): SubmitState {
|
|
217
|
+
if (!isRecord(value)) {
|
|
218
|
+
return EMPTY_SUBMIT_STATE;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const chunks = isRecord(value.chunks)
|
|
222
|
+
? Object.fromEntries(
|
|
223
|
+
Object.entries(value.chunks)
|
|
224
|
+
.filter((entry): entry is [string, number] => typeof entry[1] === "number")
|
|
225
|
+
)
|
|
226
|
+
: {};
|
|
227
|
+
|
|
228
|
+
const sessions = isRecord(value.sessions)
|
|
229
|
+
? Object.fromEntries(
|
|
230
|
+
Object.entries(value.sessions)
|
|
231
|
+
.filter((entry): entry is [string, SessionUploadState] => isSessionUploadState(entry[1]))
|
|
232
|
+
)
|
|
233
|
+
: {};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
version: 2,
|
|
237
|
+
chunks,
|
|
238
|
+
sessions,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
243
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isStoredProfileConfig(value: unknown): value is StoredProfileConfig {
|
|
247
|
+
return isRecord(value)
|
|
248
|
+
&& typeof value.apiKey === "string"
|
|
249
|
+
&& typeof value.serverUrl === "string"
|
|
250
|
+
&& typeof value.email === "string";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isSessionUploadState(value: unknown): value is SessionUploadState {
|
|
254
|
+
return isRecord(value)
|
|
255
|
+
&& (value.sourceTool === "claude_code" || value.sourceTool === "codex_cli" || value.sourceTool === "cursor")
|
|
256
|
+
&& typeof value.sourceSessionId === "string"
|
|
257
|
+
&& typeof value.locator === "string"
|
|
258
|
+
&& typeof value.nextChunkIndex === "number"
|
|
259
|
+
&& typeof value.openChunkStartTurn === "number"
|
|
260
|
+
&& typeof value.lastSeenTurnCount === "number"
|
|
261
|
+
&& (value.lastActivityAt === null || typeof value.lastActivityAt === "string")
|
|
262
|
+
&& (value.lastFlushedTurnId === null || typeof value.lastFlushedTurnId === "string");
|
|
61
263
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const CLI_NAME = "tracemp";
|
|
2
|
+
export const DEFAULT_PROFILE = "prod";
|
|
3
|
+
export const PROD_SERVER_URL = "https://trace-marketplace-api.fly.dev";
|
|
4
|
+
export const DEV_SERVER_URL = "http://localhost:3001";
|
|
5
|
+
|
|
6
|
+
export function defaultServerUrlForProfile(profile: string): string {
|
|
7
|
+
return profile === "dev" ? DEV_SERVER_URL : PROD_SERVER_URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function inferProfileFromServerUrl(serverUrl: string): string {
|
|
11
|
+
return serverUrl === PROD_SERVER_URL ? DEFAULT_PROFILE : "dev";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loginCommandForProfile(profile: string): string {
|
|
15
|
+
return profile === DEFAULT_PROFILE
|
|
16
|
+
? `${CLI_NAME} login`
|
|
17
|
+
: `${CLI_NAME} login --profile ${profile}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { NormalizedTrace, Turn } from "@tracemarketplace/shared";
|
|
3
|
+
import type { SessionSource } from "./flush.js";
|
|
4
|
+
import {
|
|
5
|
+
collectIdleSessionSources,
|
|
6
|
+
createFreshSessionState,
|
|
7
|
+
migrateLegacySessionState,
|
|
8
|
+
planSessionUploads,
|
|
9
|
+
} from "./flush.js";
|
|
10
|
+
|
|
11
|
+
function makeTurn(
|
|
12
|
+
turnId: string,
|
|
13
|
+
role: "user" | "assistant",
|
|
14
|
+
timestamp: string,
|
|
15
|
+
outputTokens = 0
|
|
16
|
+
): Turn {
|
|
17
|
+
return {
|
|
18
|
+
turn_id: turnId,
|
|
19
|
+
parent_turn_id: null,
|
|
20
|
+
role,
|
|
21
|
+
actor: role === "user" ? "human" : "assistant",
|
|
22
|
+
timestamp,
|
|
23
|
+
model: "test-model",
|
|
24
|
+
usage: {
|
|
25
|
+
input_tokens: 0,
|
|
26
|
+
output_tokens: outputTokens,
|
|
27
|
+
cache_read_input_tokens: null,
|
|
28
|
+
cache_creation_input_tokens: null,
|
|
29
|
+
reasoning_tokens: null,
|
|
30
|
+
},
|
|
31
|
+
source_metadata: {},
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: `${turnId}:${role}`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeTrace(
|
|
42
|
+
sessionId: string,
|
|
43
|
+
turns: Turn[],
|
|
44
|
+
endedAt = turns[turns.length - 1]?.timestamp ?? "2026-03-21T00:00:00.000Z"
|
|
45
|
+
): NormalizedTrace {
|
|
46
|
+
const outputTokens = turns.reduce((sum, turn) => sum + (turn.usage?.output_tokens ?? 0), 0);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
trace_id: `trace-${sessionId}`,
|
|
50
|
+
schema_version: "1.0",
|
|
51
|
+
source_tool: "codex_cli",
|
|
52
|
+
source_session_id: sessionId,
|
|
53
|
+
source_version: null,
|
|
54
|
+
submitted_by: "tester@example.com",
|
|
55
|
+
submitted_at: "2026-03-21T00:00:00.000Z",
|
|
56
|
+
extracted_at: endedAt,
|
|
57
|
+
git_branch: null,
|
|
58
|
+
cwd_hash: null,
|
|
59
|
+
working_language: null,
|
|
60
|
+
started_at: turns[0]?.timestamp ?? endedAt,
|
|
61
|
+
ended_at: endedAt,
|
|
62
|
+
turns,
|
|
63
|
+
turn_count: turns.length,
|
|
64
|
+
tool_call_count: 0,
|
|
65
|
+
has_tool_calls: false,
|
|
66
|
+
has_thinking_blocks: false,
|
|
67
|
+
has_file_changes: false,
|
|
68
|
+
has_shell_commands: false,
|
|
69
|
+
total_input_tokens: 0,
|
|
70
|
+
total_output_tokens: outputTokens,
|
|
71
|
+
total_cache_read_tokens: null,
|
|
72
|
+
content_fidelity: "full",
|
|
73
|
+
env_state: null,
|
|
74
|
+
score: null,
|
|
75
|
+
raw_r2_key: "",
|
|
76
|
+
normalized_r2_key: "",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeSource(tool: SessionSource["tool"], locator: string): SessionSource {
|
|
81
|
+
return { tool, locator, label: locator };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("planSessionUploads", () => {
|
|
85
|
+
it("flushes a sealed 100k chunk and keeps the tail pending", () => {
|
|
86
|
+
const trace = makeTrace("session-100k", [
|
|
87
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
88
|
+
makeTurn("a1", "assistant", "2026-03-21T00:01:00.000Z", 100_200),
|
|
89
|
+
makeTurn("u2", "user", "2026-03-21T00:02:00.000Z"),
|
|
90
|
+
makeTurn("a2", "assistant", "2026-03-21T00:03:00.000Z", 25),
|
|
91
|
+
]);
|
|
92
|
+
const cursor = createFreshSessionState(makeSource("codex_cli", "/tmp/session.jsonl"), trace);
|
|
93
|
+
|
|
94
|
+
const plan = planSessionUploads(trace, cursor, new Date("2026-03-21T00:04:00.000Z"));
|
|
95
|
+
|
|
96
|
+
expect(plan.uploads).toHaveLength(1);
|
|
97
|
+
expect(plan.uploads[0]?.trace.chunk_index).toBe(0);
|
|
98
|
+
expect(plan.uploads[0]?.trace.chunk_start_turn).toBe(0);
|
|
99
|
+
expect(plan.uploads[0]?.trace.chunk_complete).toBe(true);
|
|
100
|
+
expect(plan.uploads[0]?.trace.chunk_close_reason).toBe("100k_tokens");
|
|
101
|
+
expect(plan.pending).toBe(true);
|
|
102
|
+
expect(plan.uploads[0]?.nextState.openChunkStartTurn).toBe(2);
|
|
103
|
+
expect(plan.uploads[0]?.nextState.nextChunkIndex).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("flushes an under-100k tail after two days of inactivity", () => {
|
|
107
|
+
const trace = makeTrace("session-idle", [
|
|
108
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
109
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 40),
|
|
110
|
+
], "2026-03-21T00:05:00.000Z");
|
|
111
|
+
const cursor = createFreshSessionState(makeSource("codex_cli", "/tmp/session.jsonl"), trace);
|
|
112
|
+
|
|
113
|
+
const plan = planSessionUploads(trace, cursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
114
|
+
|
|
115
|
+
expect(plan.uploads).toHaveLength(1);
|
|
116
|
+
expect(plan.uploads[0]?.trace.chunk_index).toBe(0);
|
|
117
|
+
expect(plan.uploads[0]?.trace.chunk_close_reason).toBe("idle_2d");
|
|
118
|
+
expect(plan.pending).toBe(false);
|
|
119
|
+
expect(plan.uploads[0]?.nextState.openChunkStartTurn).toBe(trace.turn_count);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("creates a later chunk when a session resumes after an idle-finalized chunk", () => {
|
|
123
|
+
const initialTrace = makeTrace("session-resume", [
|
|
124
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
125
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 20),
|
|
126
|
+
], "2026-03-21T00:05:00.000Z");
|
|
127
|
+
const source = makeSource("codex_cli", "/tmp/session.jsonl");
|
|
128
|
+
const initialCursor = createFreshSessionState(source, initialTrace);
|
|
129
|
+
const initialPlan = planSessionUploads(initialTrace, initialCursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
130
|
+
const finalizedCursor = initialPlan.uploads[0]?.nextState;
|
|
131
|
+
|
|
132
|
+
if (!finalizedCursor) {
|
|
133
|
+
throw new Error("expected initial chunk upload");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resumedTrace = makeTrace("session-resume", [
|
|
137
|
+
...initialTrace.turns,
|
|
138
|
+
makeTurn("u2", "user", "2026-03-24T00:00:00.000Z"),
|
|
139
|
+
makeTurn("a2", "assistant", "2026-03-24T00:10:00.000Z", 15),
|
|
140
|
+
], "2026-03-24T00:10:00.000Z");
|
|
141
|
+
|
|
142
|
+
const resumedPlan = planSessionUploads(resumedTrace, finalizedCursor, new Date("2026-03-26T00:11:00.000Z"));
|
|
143
|
+
|
|
144
|
+
expect(resumedPlan.uploads).toHaveLength(1);
|
|
145
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_index).toBe(1);
|
|
146
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_start_turn).toBe(2);
|
|
147
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_close_reason).toBe("idle_2d");
|
|
148
|
+
expect(resumedPlan.pending).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does not re-upload a legacy session on first migration", () => {
|
|
152
|
+
const trace = makeTrace("legacy-session", [
|
|
153
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
154
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 55),
|
|
155
|
+
]);
|
|
156
|
+
const migratedCursor = migrateLegacySessionState(
|
|
157
|
+
makeSource("codex_cli", "/tmp/legacy.jsonl"),
|
|
158
|
+
trace,
|
|
159
|
+
0
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const plan = planSessionUploads(trace, migratedCursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
163
|
+
|
|
164
|
+
expect(plan.uploads).toHaveLength(0);
|
|
165
|
+
expect(plan.pending).toBe(false);
|
|
166
|
+
expect(plan.observedState.nextChunkIndex).toBe(1);
|
|
167
|
+
expect(plan.observedState.openChunkStartTurn).toBe(trace.turn_count);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("collectIdleSessionSources", () => {
|
|
172
|
+
it("returns only tracked sessions with an open tail older than two days", () => {
|
|
173
|
+
const sources = collectIdleSessionSources({
|
|
174
|
+
"codex_cli:idle": {
|
|
175
|
+
sourceTool: "codex_cli",
|
|
176
|
+
sourceSessionId: "idle",
|
|
177
|
+
locator: "/tmp/idle.jsonl",
|
|
178
|
+
nextChunkIndex: 1,
|
|
179
|
+
openChunkStartTurn: 2,
|
|
180
|
+
lastSeenTurnCount: 4,
|
|
181
|
+
lastActivityAt: "2026-03-21T00:00:00.000Z",
|
|
182
|
+
lastFlushedTurnId: "a1",
|
|
183
|
+
},
|
|
184
|
+
"codex_cli:closed": {
|
|
185
|
+
sourceTool: "codex_cli",
|
|
186
|
+
sourceSessionId: "closed",
|
|
187
|
+
locator: "/tmp/closed.jsonl",
|
|
188
|
+
nextChunkIndex: 1,
|
|
189
|
+
openChunkStartTurn: 4,
|
|
190
|
+
lastSeenTurnCount: 4,
|
|
191
|
+
lastActivityAt: "2026-03-21T00:00:00.000Z",
|
|
192
|
+
lastFlushedTurnId: "a2",
|
|
193
|
+
},
|
|
194
|
+
"codex_cli:fresh": {
|
|
195
|
+
sourceTool: "codex_cli",
|
|
196
|
+
sourceSessionId: "fresh",
|
|
197
|
+
locator: "/tmp/fresh.jsonl",
|
|
198
|
+
nextChunkIndex: 0,
|
|
199
|
+
openChunkStartTurn: 0,
|
|
200
|
+
lastSeenTurnCount: 2,
|
|
201
|
+
lastActivityAt: "2026-03-22T23:59:59.000Z",
|
|
202
|
+
lastFlushedTurnId: null,
|
|
203
|
+
},
|
|
204
|
+
}, new Date("2026-03-23T00:01:00.000Z"));
|
|
205
|
+
|
|
206
|
+
expect(sources).toEqual([
|
|
207
|
+
{
|
|
208
|
+
tool: "codex_cli",
|
|
209
|
+
locator: "/tmp/idle.jsonl",
|
|
210
|
+
label: "codex_cli:idle",
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
});
|