iteration-cli 0.4.4 → 0.6.0
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/README.md +8 -2
- package/dist/auto-connect.d.ts +30 -0
- package/dist/auto-connect.js +220 -0
- package/dist/claude-transcript.d.ts +6 -0
- package/dist/claude-transcript.js +255 -0
- package/dist/clerk-device-auth.d.ts +3 -1
- package/dist/clerk-device-auth.js +7 -4
- package/dist/cli.js +269 -66
- package/dist/collector.d.ts +1 -1
- package/dist/collector.js +32 -14
- package/dist/history-import.d.ts +1 -1
- package/dist/history-import.js +18 -15
- package/dist/launch-agent.d.ts +5 -4
- package/dist/launch-agent.js +26 -23
- package/dist/rollout.js +122 -135
- package/dist/service.d.ts +2 -1
- package/dist/service.js +59 -5
- package/dist/status-view.d.ts +4 -2
- package/dist/status-view.js +44 -37
- package/dist/storage.d.ts +22 -10
- package/dist/storage.js +144 -51
- package/dist/turn-summary.d.ts +21 -0
- package/dist/turn-summary.js +302 -0
- package/package.json +2 -2
package/dist/status-view.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { collectorSourceLabel, } from "iteration-trace-contracts";
|
|
2
|
+
function sourceName(source) {
|
|
3
|
+
return collectorSourceLabel(source);
|
|
4
|
+
}
|
|
5
|
+
export function safeWorkspaceName(value) {
|
|
2
6
|
const cleaned = value
|
|
3
7
|
?.split("")
|
|
4
8
|
.map((character) => {
|
|
@@ -41,11 +45,11 @@ function relativeTime(atMs, nowMs) {
|
|
|
41
45
|
function pendingDescription(status) {
|
|
42
46
|
return `${status.pendingEvents} event${status.pendingEvents === 1 ? "" : "s"} (${readableBytes(status.pendingBytes)}) waiting on this Mac`;
|
|
43
47
|
}
|
|
44
|
-
function progressDescription(scan) {
|
|
48
|
+
function progressDescription(scan, source) {
|
|
45
49
|
if (scan.kind === "waiting")
|
|
46
50
|
return "Local history is preparing to scan.";
|
|
47
51
|
if (scan.totalBytes === 0)
|
|
48
|
-
return
|
|
52
|
+
return `No local ${sourceName(source)} sessions found yet.`;
|
|
49
53
|
if (scan.scannedBytes >= scan.totalBytes)
|
|
50
54
|
return `Local history scanned (${readableBytes(scan.totalBytes)}).`;
|
|
51
55
|
const remainingBytes = scan.totalBytes - scan.scannedBytes;
|
|
@@ -54,9 +58,9 @@ function progressDescription(scan) {
|
|
|
54
58
|
const percentage = Math.min(99, Math.floor((scan.scannedBytes / scan.totalBytes) * 100));
|
|
55
59
|
return `About ${percentage}% of local history scanned (${readableBytes(scan.scannedBytes)} of ${readableBytes(scan.totalBytes)}).`;
|
|
56
60
|
}
|
|
57
|
-
function historyDescription(status) {
|
|
61
|
+
function historyDescription(status, source) {
|
|
58
62
|
if (status.health.historyCaughtUpAtMs === null) {
|
|
59
|
-
return progressDescription(status.scan);
|
|
63
|
+
return progressDescription(status.scan, source);
|
|
60
64
|
}
|
|
61
65
|
if (status.scan.kind === "waiting")
|
|
62
66
|
return "History is preparing to scan.";
|
|
@@ -67,7 +71,8 @@ function historyDescription(status) {
|
|
|
67
71
|
return "History imported; scanning current activity.";
|
|
68
72
|
return `History imported; ${readableBytes(remainingBytes)} of new activity waiting to scan.`;
|
|
69
73
|
}
|
|
70
|
-
function failureDescription(status) {
|
|
74
|
+
function failureDescription(status, source) {
|
|
75
|
+
const name = sourceName(source);
|
|
71
76
|
const failure = status.health.lastFailure;
|
|
72
77
|
if (failure === null) {
|
|
73
78
|
throw new Error("A collector failure is required to describe it.");
|
|
@@ -76,7 +81,7 @@ function failureDescription(status) {
|
|
|
76
81
|
const code = failure.status === null ? "HTTP error" : `HTTP ${failure.status}`;
|
|
77
82
|
if (failure.status === 401 || failure.status === 403) {
|
|
78
83
|
return {
|
|
79
|
-
heading:
|
|
84
|
+
heading: `${name} upload access needs attention`,
|
|
80
85
|
explanation: `Iteration denied this collector (${code}). Queued data stays on this Mac. Contact Iteration support to reconnect it safely.`,
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -84,51 +89,52 @@ function failureDescription(status) {
|
|
|
84
89
|
failure.status < 500 &&
|
|
85
90
|
failure.status !== 429) {
|
|
86
91
|
return {
|
|
87
|
-
heading:
|
|
92
|
+
heading: `${name} uploads need attention`,
|
|
88
93
|
explanation: `Iteration rejected an event batch (${code}). Check the collector log for details.`,
|
|
89
94
|
};
|
|
90
95
|
}
|
|
91
96
|
return {
|
|
92
|
-
heading:
|
|
97
|
+
heading: `${name} uploads are delayed`,
|
|
93
98
|
explanation: `Iteration could not accept an upload (${code}). Retrying automatically.`,
|
|
94
99
|
};
|
|
95
100
|
}
|
|
96
101
|
if (failure.kind === "network") {
|
|
97
102
|
return {
|
|
98
|
-
heading:
|
|
103
|
+
heading: `${name} uploads are delayed`,
|
|
99
104
|
explanation: "Could not reach Iteration. Retrying automatically.",
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
107
|
if (failure.kind === "response") {
|
|
103
108
|
return {
|
|
104
|
-
heading:
|
|
109
|
+
heading: `${name} uploads need attention`,
|
|
105
110
|
explanation: "Iteration returned an unexpected upload response. Retrying automatically.",
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
return {
|
|
109
|
-
heading:
|
|
110
|
-
explanation:
|
|
114
|
+
heading: `${name} collector needs attention`,
|
|
115
|
+
explanation: `A local error interrupted collection. See iteration status ${source} for details.`,
|
|
111
116
|
};
|
|
112
117
|
}
|
|
113
|
-
export function renderHome(status, nowMs = Date.now()) {
|
|
118
|
+
export function renderHome(status, nowMs = Date.now(), source = "codex") {
|
|
119
|
+
const name = sourceName(source);
|
|
114
120
|
if (status.kind === "not_connected") {
|
|
115
121
|
return [
|
|
116
122
|
"Iteration",
|
|
117
123
|
"",
|
|
118
|
-
|
|
119
|
-
"Run iteration connect
|
|
124
|
+
`${name} is not connected.`,
|
|
125
|
+
"Run iteration connect to import your existing sessions.",
|
|
120
126
|
].join("\n");
|
|
121
127
|
}
|
|
122
128
|
if (status.kind === "needs_repair") {
|
|
123
129
|
return [
|
|
124
130
|
"Iteration",
|
|
125
131
|
"",
|
|
126
|
-
|
|
132
|
+
`${name} connection needs repair`,
|
|
127
133
|
"Local collector data exists, but its connection details are missing.",
|
|
128
134
|
status.pendingBytes > 0
|
|
129
135
|
? `${readableBytes(status.pendingBytes)} of queued data remains on this Mac.`
|
|
130
136
|
: "Existing local progress remains on this Mac.",
|
|
131
|
-
|
|
137
|
+
`Run iteration status ${source} for recovery details.`,
|
|
132
138
|
].join("\n");
|
|
133
139
|
}
|
|
134
140
|
const workspaceName = safeWorkspaceName(status.workspaceName);
|
|
@@ -137,54 +143,54 @@ export function renderHome(status, nowMs = Date.now()) {
|
|
|
137
143
|
"",
|
|
138
144
|
];
|
|
139
145
|
if (!status.running) {
|
|
140
|
-
lines.push(
|
|
146
|
+
lines.push(`${name} is stopped`, "New sessions are not being collected.");
|
|
141
147
|
if (status.pendingEvents > 0)
|
|
142
148
|
lines.push(`${pendingDescription(status)}.`);
|
|
143
|
-
lines.push(
|
|
149
|
+
lines.push(`Resume with iteration start ${source}.`);
|
|
144
150
|
}
|
|
145
151
|
else if (!status.healthAvailable) {
|
|
146
|
-
lines.push(
|
|
152
|
+
lines.push(`${name} is running; local status needs attention`, "Upload history could not be read. Collection continues in the background.");
|
|
147
153
|
if (status.pendingEvents > 0)
|
|
148
154
|
lines.push(`${pendingDescription(status)}.`);
|
|
149
|
-
lines.push(
|
|
155
|
+
lines.push(`Details: iteration status ${source}`);
|
|
150
156
|
}
|
|
151
157
|
else if (status.health.lastFailure !== null) {
|
|
152
|
-
const failure = failureDescription(status);
|
|
158
|
+
const failure = failureDescription(status, source);
|
|
153
159
|
lines.push(failure.heading, failure.explanation);
|
|
154
160
|
if (status.pendingEvents > 0)
|
|
155
161
|
lines.push(`${pendingDescription(status)}.`);
|
|
156
162
|
if (status.health.lastUploadAtMs !== null) {
|
|
157
163
|
lines.push(`Last successful upload ${relativeTime(status.health.lastUploadAtMs, nowMs)}.`);
|
|
158
164
|
}
|
|
159
|
-
lines.push(
|
|
165
|
+
lines.push(`Details: iteration status ${source}`);
|
|
160
166
|
}
|
|
161
167
|
else if (status.scan.kind === "waiting") {
|
|
162
|
-
lines.push(
|
|
168
|
+
lines.push(`${name} is preparing your history`, progressDescription(status.scan, source));
|
|
163
169
|
}
|
|
164
170
|
else if (status.scan.kind === "scanning" &&
|
|
165
171
|
status.scan.totalBytes > 0 &&
|
|
166
172
|
status.scan.scannedBytes < status.scan.totalBytes &&
|
|
167
173
|
status.health.historyCaughtUpAtMs === null) {
|
|
168
|
-
lines.push(
|
|
174
|
+
lines.push(`${name} is importing existing sessions`, historyDescription(status, source), "New activity is collected automatically.");
|
|
169
175
|
}
|
|
170
176
|
else if (status.scan.kind === "scanning" &&
|
|
171
177
|
status.scan.totalBytes - status.scan.scannedBytes >= 1_000_000) {
|
|
172
|
-
lines.push(
|
|
178
|
+
lines.push(`${name} is catching up with new activity`, historyDescription(status, source));
|
|
173
179
|
if (status.pendingEvents > 0)
|
|
174
180
|
lines.push(`${pendingDescription(status)}.`);
|
|
175
181
|
}
|
|
176
182
|
else if (status.pendingEvents > 0) {
|
|
177
|
-
lines.push(
|
|
183
|
+
lines.push(`${name} is uploading captured sessions`, `${pendingDescription(status)}.`);
|
|
178
184
|
}
|
|
179
185
|
else if (status.scan.totalBytes === 0) {
|
|
180
|
-
lines.push(
|
|
186
|
+
lines.push(`${name} is connected`, `Waiting for ${name} sessions on this Mac.`);
|
|
181
187
|
}
|
|
182
188
|
else if (status.scan.kind === "scanning" &&
|
|
183
189
|
status.scan.scannedBytes < status.scan.totalBytes) {
|
|
184
|
-
lines.push(
|
|
190
|
+
lines.push(`${name} is collecting new activity`, "Local history is imported; current activity is being scanned.");
|
|
185
191
|
}
|
|
186
192
|
else {
|
|
187
|
-
lines.push(
|
|
193
|
+
lines.push(`${name} is collecting`, "Local scan is caught up and no uploads are waiting.", "New activity is collected automatically.");
|
|
188
194
|
}
|
|
189
195
|
if (status.running &&
|
|
190
196
|
status.health.lastFailure === null &&
|
|
@@ -194,13 +200,14 @@ export function renderHome(status, nowMs = Date.now()) {
|
|
|
194
200
|
lines.push("", `Explore traces ${status.tracesUrl}`);
|
|
195
201
|
return lines.join("\n");
|
|
196
202
|
}
|
|
197
|
-
export function renderDetailedStatus(status, nowMs = Date.now()) {
|
|
203
|
+
export function renderDetailedStatus(status, nowMs = Date.now(), source = "codex") {
|
|
204
|
+
const name = sourceName(source);
|
|
198
205
|
if (status.kind === "not_connected") {
|
|
199
|
-
return
|
|
206
|
+
return `${name} is not connected. Run iteration connect to connect.`;
|
|
200
207
|
}
|
|
201
208
|
if (status.kind === "needs_repair") {
|
|
202
209
|
return [
|
|
203
|
-
|
|
210
|
+
`${name} connection needs repair`,
|
|
204
211
|
"Local collector data exists without its connection details.",
|
|
205
212
|
`Queued data: ${readableBytes(status.pendingBytes)}`,
|
|
206
213
|
`Restore the missing config from backup: ${status.configPath}`,
|
|
@@ -209,10 +216,10 @@ export function renderDetailedStatus(status, nowMs = Date.now()) {
|
|
|
209
216
|
}
|
|
210
217
|
const workspaceName = safeWorkspaceName(status.workspaceName);
|
|
211
218
|
const lines = [
|
|
212
|
-
|
|
219
|
+
`${name} collector`,
|
|
213
220
|
...(workspaceName === null ? [] : [`Workspace: ${workspaceName}`]),
|
|
214
221
|
`Background process: ${status.running ? "Running" : "Stopped"}`,
|
|
215
|
-
`History: ${historyDescription(status).replace(/^History /, "")}`,
|
|
222
|
+
`History: ${historyDescription(status, source).replace(/^History /, "")}`,
|
|
216
223
|
`Uploads: ${status.pendingEvents === 0 ? "None waiting" : pendingDescription(status)}`,
|
|
217
224
|
];
|
|
218
225
|
if (!status.healthAvailable) {
|
|
@@ -233,7 +240,7 @@ export function renderDetailedStatus(status, nowMs = Date.now()) {
|
|
|
233
240
|
lines.push(`Last issue: ${label} (${relativeTime(failure.atMs, nowMs)})`, `Collector log: ${status.logPath}`);
|
|
234
241
|
}
|
|
235
242
|
if (!status.running)
|
|
236
|
-
lines.push(
|
|
243
|
+
lines.push(`Resume: iteration start ${source}`);
|
|
237
244
|
lines.push(`Explore traces: ${status.tracesUrl}`);
|
|
238
245
|
return lines.join("\n");
|
|
239
246
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { type TraceEvent } from "iteration-trace-contracts";
|
|
2
|
-
|
|
1
|
+
import { type CollectorSource, type TraceEvent } from "iteration-trace-contracts";
|
|
2
|
+
type CollectorConnection = Readonly<{
|
|
3
3
|
endpoint: string;
|
|
4
4
|
token: string;
|
|
5
|
-
codexHome: string;
|
|
6
5
|
dataDirectory: string;
|
|
6
|
+
organizationId?: string;
|
|
7
7
|
organizationName?: string;
|
|
8
8
|
}>;
|
|
9
|
+
export type CollectorConfig = (CollectorConnection & Readonly<{
|
|
10
|
+
codexHome: string;
|
|
11
|
+
}>) | (CollectorConnection & Readonly<{
|
|
12
|
+
source: "claude";
|
|
13
|
+
claudeHome: string;
|
|
14
|
+
}>);
|
|
15
|
+
export declare function collectorSource(config: CollectorConfig): CollectorSource;
|
|
16
|
+
export declare function collectorSourceDirectory(config: CollectorConfig): string;
|
|
9
17
|
export type CollectorState = Readonly<{
|
|
10
18
|
cursors: Readonly<Record<string, number>>;
|
|
11
19
|
skipping: readonly string[];
|
|
@@ -37,9 +45,10 @@ export type CollectorPaths = Readonly<{
|
|
|
37
45
|
historyBackfill: string;
|
|
38
46
|
health: string;
|
|
39
47
|
}>;
|
|
48
|
+
export declare function defaultCollectorDirectory(source: CollectorSource): string;
|
|
40
49
|
export declare function validateCollectorEndpoint(endpoint: string): URL;
|
|
41
50
|
export declare function collectorPaths(directory?: string): CollectorPaths;
|
|
42
|
-
export declare function assertFreshCollectorDirectory(paths: CollectorPaths): Promise<void>;
|
|
51
|
+
export declare function assertFreshCollectorDirectory(paths: CollectorPaths, source?: CollectorSource): Promise<void>;
|
|
43
52
|
export declare function ensurePrivateDirectory(directory: string): Promise<void>;
|
|
44
53
|
export declare function saveConfig(config: CollectorConfig, paths: CollectorPaths): Promise<void>;
|
|
45
54
|
export declare function loadHealth(paths: CollectorPaths): Promise<CollectorHealth>;
|
|
@@ -53,23 +62,26 @@ export declare function pendingEventIds(paths: CollectorPaths): Promise<Set<stri
|
|
|
53
62
|
export declare function appendEvents(events: readonly TraceEvent[], paths: CollectorPaths): Promise<void>;
|
|
54
63
|
export declare function nextBatch(paths: CollectorPaths, maxBytes: number): Promise<readonly TraceEvent[]>;
|
|
55
64
|
export declare function acknowledgeEvents(acknowledgedIds: ReadonlySet<string>, paths: CollectorPaths): Promise<void>;
|
|
56
|
-
export declare function initializeState(
|
|
57
|
-
export declare function
|
|
65
|
+
export declare function initializeState(source: CollectorSource, home: string, paths: CollectorPaths, includeHistory: boolean): Promise<void>;
|
|
66
|
+
export declare function prepareSourceInitialState(source: CollectorSource, home: string, includeHistory: boolean): Promise<CollectorState>;
|
|
58
67
|
export declare function ensureStartableState(config: CollectorConfig, paths: CollectorPaths): Promise<boolean>;
|
|
59
68
|
export declare function ensureHistoryBackfill(codexHome: string, paths: CollectorPaths): Promise<boolean>;
|
|
60
|
-
export declare function
|
|
69
|
+
export declare function ensureClaudeHistoryBackfill(claudeHome: string, paths: CollectorPaths): Promise<boolean>;
|
|
70
|
+
export declare function historyScanProgress(home: string, paths: CollectorPaths, source?: CollectorSource): Promise<Readonly<{
|
|
61
71
|
kind: "waiting";
|
|
62
72
|
} | {
|
|
63
73
|
kind: "scanning";
|
|
64
74
|
scannedBytes: number;
|
|
65
75
|
totalBytes: number;
|
|
66
76
|
}>>;
|
|
67
|
-
export declare function resetHistoryCursors(
|
|
77
|
+
export declare function resetHistoryCursors(home: string, paths: CollectorPaths, source?: CollectorSource): Promise<Readonly<{
|
|
68
78
|
files: number;
|
|
69
79
|
bytes: number;
|
|
70
80
|
}>>;
|
|
71
|
-
export declare function historyImportSize(
|
|
81
|
+
export declare function historyImportSize(home: string, source?: CollectorSource): Promise<Readonly<{
|
|
72
82
|
files: number;
|
|
73
83
|
bytes: number;
|
|
74
84
|
}>>;
|
|
75
|
-
export declare function
|
|
85
|
+
export declare function claudeFiles(root: string): Promise<string[]>;
|
|
86
|
+
export declare function sourceFiles(source: CollectorSource, home: string): Promise<string[]>;
|
|
87
|
+
export {};
|
package/dist/storage.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
1
2
|
import { createWriteStream, promises as fs } from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { once } from "node:events";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import * as z from "zod";
|
|
7
6
|
import { MAX_TRACE_EVENTS_PER_BATCH, parseTraceEventBatch, } from "iteration-trace-contracts";
|
|
7
|
+
import * as z from "zod";
|
|
8
8
|
const MAX_OUTBOX_BYTES = 64 * 1024 * 1024;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export function collectorSource(config) {
|
|
10
|
+
return "claudeHome" in config ? "claude" : "codex";
|
|
11
|
+
}
|
|
12
|
+
export function collectorSourceDirectory(config) {
|
|
13
|
+
return "claudeHome" in config ? config.claudeHome : config.codexHome;
|
|
14
|
+
}
|
|
15
|
+
const connectionSchema = {
|
|
11
16
|
endpoint: z.string(),
|
|
12
17
|
token: z.string(),
|
|
13
|
-
codexHome: z.string(),
|
|
14
18
|
dataDirectory: z.string(),
|
|
19
|
+
organizationId: z.uuid().optional(),
|
|
15
20
|
organizationName: z.string().min(1).max(160).optional(),
|
|
16
|
-
}
|
|
17
|
-
|
|
21
|
+
};
|
|
22
|
+
const configSchema = z.union([
|
|
23
|
+
z.object({ ...connectionSchema, codexHome: z.string() }).strict(),
|
|
24
|
+
z
|
|
25
|
+
.object({
|
|
26
|
+
...connectionSchema,
|
|
27
|
+
source: z.literal("claude"),
|
|
28
|
+
claudeHome: z.string(),
|
|
29
|
+
})
|
|
30
|
+
.strict(),
|
|
31
|
+
]);
|
|
18
32
|
const stateSchema = z
|
|
19
33
|
.object({
|
|
20
34
|
cursors: z.record(z.string(), z.number().int().min(0).max(Number.MAX_SAFE_INTEGER)),
|
|
@@ -55,8 +69,8 @@ function parseOutboxEvent(line) {
|
|
|
55
69
|
throw new Error("Trace outbox event is missing.");
|
|
56
70
|
return event;
|
|
57
71
|
}
|
|
58
|
-
function
|
|
59
|
-
return path.join(os.homedir(), ".config", "iteration",
|
|
72
|
+
export function defaultCollectorDirectory(source) {
|
|
73
|
+
return path.join(os.homedir(), ".config", "iteration", `${source}-collector`);
|
|
60
74
|
}
|
|
61
75
|
export function validateCollectorEndpoint(endpoint) {
|
|
62
76
|
let parsed;
|
|
@@ -82,7 +96,7 @@ export function validateCollectorEndpoint(endpoint) {
|
|
|
82
96
|
}
|
|
83
97
|
return parsed;
|
|
84
98
|
}
|
|
85
|
-
export function collectorPaths(directory =
|
|
99
|
+
export function collectorPaths(directory = defaultCollectorDirectory("codex")) {
|
|
86
100
|
return {
|
|
87
101
|
config: path.join(directory, "config.json"),
|
|
88
102
|
state: path.join(directory, "state.json"),
|
|
@@ -92,7 +106,7 @@ export function collectorPaths(directory = defaultDirectory()) {
|
|
|
92
106
|
health: path.join(directory, "health.json"),
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
|
-
export async function assertFreshCollectorDirectory(paths) {
|
|
109
|
+
export async function assertFreshCollectorDirectory(paths, source = "codex") {
|
|
96
110
|
const state = await loadState(paths);
|
|
97
111
|
const configExists = await fs
|
|
98
112
|
.access(paths.config)
|
|
@@ -103,7 +117,7 @@ export async function assertFreshCollectorDirectory(paths) {
|
|
|
103
117
|
throw error;
|
|
104
118
|
});
|
|
105
119
|
if (configExists) {
|
|
106
|
-
throw new Error(
|
|
120
|
+
throw new Error(`This collector data directory is already configured. Run iteration status ${source} or iteration start ${source} to repair startup. Use a different --data-directory for another workspace, or iteration import-history ${source} for earlier sessions.`);
|
|
107
121
|
}
|
|
108
122
|
if (state.initialized || (await outboxSize(paths)) > 0) {
|
|
109
123
|
throw new Error("Collector state or pending events exist without a config. Use a different --data-directory; this data cannot be safely assigned to a new workspace.");
|
|
@@ -202,12 +216,12 @@ export async function loadConfig(paths) {
|
|
|
202
216
|
throw new Error("Collector config has an invalid shape.", {
|
|
203
217
|
cause: parsed.error,
|
|
204
218
|
});
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
validateCollectorEndpoint(parsed.data.endpoint);
|
|
220
|
+
const { token } = parsed.data;
|
|
207
221
|
if (token.length !== 43 || !/^[A-Za-z0-9_-]+$/.test(token)) {
|
|
208
222
|
throw new Error("Collector token has an invalid format.");
|
|
209
223
|
}
|
|
210
|
-
return
|
|
224
|
+
return parsed.data;
|
|
211
225
|
}
|
|
212
226
|
export async function loadState(paths) {
|
|
213
227
|
let contents;
|
|
@@ -372,24 +386,23 @@ export async function acknowledgeEvents(acknowledgedIds, paths) {
|
|
|
372
386
|
throw error;
|
|
373
387
|
}
|
|
374
388
|
}
|
|
375
|
-
export async function initializeState(
|
|
389
|
+
export async function initializeState(source, home, paths, includeHistory) {
|
|
376
390
|
const state = await loadState(paths);
|
|
377
|
-
const sourceDirectory = path.resolve(
|
|
391
|
+
const sourceDirectory = path.resolve(home);
|
|
378
392
|
if (state.initialized && state.sourceDirectory !== sourceDirectory) {
|
|
379
|
-
throw new Error(
|
|
393
|
+
throw new Error(`Collector state belongs to another ${source} directory; use a separate --data-directory.`);
|
|
380
394
|
}
|
|
381
395
|
if (state.initialized && state.sourceDirectory === sourceDirectory) {
|
|
382
396
|
if (includeHistory) {
|
|
383
|
-
throw new Error(
|
|
397
|
+
throw new Error(`${source} is already connected. Run iteration import-history ${source} to import existing sessions.`);
|
|
384
398
|
}
|
|
385
399
|
return;
|
|
386
400
|
}
|
|
387
|
-
await saveState(await
|
|
401
|
+
await saveState(await prepareSourceInitialState(source, home, includeHistory), paths);
|
|
388
402
|
}
|
|
389
|
-
export async function
|
|
390
|
-
const sourceDirectory = path.resolve(
|
|
391
|
-
const
|
|
392
|
-
const files = await rolloutFiles(sessionsRoot);
|
|
403
|
+
export async function prepareSourceInitialState(source, home, includeHistory) {
|
|
404
|
+
const sourceDirectory = path.resolve(home);
|
|
405
|
+
const files = await sourceFiles(source, sourceDirectory);
|
|
393
406
|
const cursors = {};
|
|
394
407
|
if (!includeHistory) {
|
|
395
408
|
for (const filename of files) {
|
|
@@ -404,20 +417,26 @@ export async function prepareInitialState(codexHome, includeHistory) {
|
|
|
404
417
|
};
|
|
405
418
|
}
|
|
406
419
|
export async function ensureStartableState(config, paths) {
|
|
420
|
+
const source = collectorSource(config);
|
|
421
|
+
const directory = collectorSourceDirectory(config);
|
|
407
422
|
const state = await loadState(paths);
|
|
408
423
|
if (state.initialized) {
|
|
409
|
-
if (state.sourceDirectory !== path.resolve(
|
|
410
|
-
throw new Error(
|
|
424
|
+
if (state.sourceDirectory !== path.resolve(directory)) {
|
|
425
|
+
throw new Error(`Collector state belongs to another ${source} directory; use a separate --data-directory.`);
|
|
411
426
|
}
|
|
412
427
|
return false;
|
|
413
428
|
}
|
|
414
429
|
if ((await outboxSize(paths)) > 0) {
|
|
415
430
|
throw new Error("Pending events exist without an initialized checkpoint; repair the collector state before starting uploads.");
|
|
416
431
|
}
|
|
417
|
-
await
|
|
432
|
+
await saveState(await prepareSourceInitialState(source, directory, true), paths);
|
|
418
433
|
return true;
|
|
419
434
|
}
|
|
420
|
-
|
|
435
|
+
const HISTORY_BACKFILL_VERSION = 4;
|
|
436
|
+
const historyBackfillMarkerSchema = z
|
|
437
|
+
.object({ version: z.number().int().min(1).max(HISTORY_BACKFILL_VERSION) })
|
|
438
|
+
.strict();
|
|
439
|
+
async function readHistoryBackfillVersion(paths) {
|
|
421
440
|
let marker;
|
|
422
441
|
try {
|
|
423
442
|
marker = await fs.readFile(paths.historyBackfill, "utf8");
|
|
@@ -425,18 +444,28 @@ async function hasHistoryBackfillMarker(paths) {
|
|
|
425
444
|
catch (error) {
|
|
426
445
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
427
446
|
throw error;
|
|
428
|
-
|
|
447
|
+
return null;
|
|
429
448
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
449
|
+
let decoded;
|
|
450
|
+
try {
|
|
451
|
+
decoded = JSON.parse(marker);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
throw new Error("Collector history backfill marker is not valid JSON.", {
|
|
455
|
+
cause: error,
|
|
456
|
+
});
|
|
435
457
|
}
|
|
436
|
-
|
|
458
|
+
const parsed = historyBackfillMarkerSchema.safeParse(decoded);
|
|
459
|
+
if (!parsed.success) {
|
|
460
|
+
throw new Error("Collector history backfill marker has an invalid shape.", {
|
|
461
|
+
cause: parsed.error,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return parsed.data.version;
|
|
437
465
|
}
|
|
438
466
|
export async function ensureHistoryBackfill(codexHome, paths) {
|
|
439
|
-
|
|
467
|
+
const version = await readHistoryBackfillVersion(paths);
|
|
468
|
+
if (version !== null && version >= HISTORY_BACKFILL_VERSION)
|
|
440
469
|
return false;
|
|
441
470
|
const state = await loadState(paths);
|
|
442
471
|
if (!state.initialized || state.sourceDirectory !== path.resolve(codexHome)) {
|
|
@@ -444,17 +473,33 @@ export async function ensureHistoryBackfill(codexHome, paths) {
|
|
|
444
473
|
}
|
|
445
474
|
await rolloutFiles(path.join(codexHome, "sessions"));
|
|
446
475
|
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
476
|
+
await writeJsonAtomic(paths.historyBackfill, {
|
|
477
|
+
version: HISTORY_BACKFILL_VERSION,
|
|
478
|
+
});
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
export async function ensureClaudeHistoryBackfill(claudeHome, paths) {
|
|
482
|
+
const version = await readHistoryBackfillVersion(paths);
|
|
483
|
+
if (version !== null && version >= 1)
|
|
484
|
+
return false;
|
|
485
|
+
const state = await loadState(paths);
|
|
486
|
+
if (!state.initialized ||
|
|
487
|
+
state.sourceDirectory !== path.resolve(claudeHome)) {
|
|
488
|
+
throw new Error("Collector state does not match the Claude directory; reconnect Claude before uploading.");
|
|
489
|
+
}
|
|
490
|
+
await claudeFiles(path.join(claudeHome, "projects"));
|
|
491
|
+
await saveState({ ...state, cursors: {}, skipping: [] }, paths);
|
|
447
492
|
await writeJsonAtomic(paths.historyBackfill, { version: 1 });
|
|
448
493
|
return true;
|
|
449
494
|
}
|
|
450
|
-
export async function historyScanProgress(
|
|
451
|
-
if (
|
|
495
|
+
export async function historyScanProgress(home, paths, source = "codex") {
|
|
496
|
+
if ((await readHistoryBackfillVersion(paths)) === null)
|
|
452
497
|
return { kind: "waiting" };
|
|
453
498
|
const state = await loadState(paths);
|
|
454
|
-
if (!state.initialized || state.sourceDirectory !== path.resolve(
|
|
455
|
-
throw new Error(
|
|
499
|
+
if (!state.initialized || state.sourceDirectory !== path.resolve(home)) {
|
|
500
|
+
throw new Error(`Collector state does not match the ${source} directory.`);
|
|
456
501
|
}
|
|
457
|
-
const files = await
|
|
502
|
+
const files = await sourceFiles(source, home);
|
|
458
503
|
let scannedBytes = 0;
|
|
459
504
|
let totalBytes = 0;
|
|
460
505
|
for (let start = 0; start < files.length; start += 64) {
|
|
@@ -482,26 +527,26 @@ export async function historyScanProgress(codexHome, paths) {
|
|
|
482
527
|
scannedBytes += cursor <= size ? cursor : 0;
|
|
483
528
|
if (!Number.isSafeInteger(totalBytes) ||
|
|
484
529
|
!Number.isSafeInteger(scannedBytes)) {
|
|
485
|
-
throw new Error(
|
|
530
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
486
531
|
}
|
|
487
532
|
}
|
|
488
533
|
}
|
|
489
534
|
return { kind: "scanning", scannedBytes, totalBytes };
|
|
490
535
|
}
|
|
491
|
-
export async function resetHistoryCursors(
|
|
536
|
+
export async function resetHistoryCursors(home, paths, source = "codex") {
|
|
492
537
|
const state = await loadState(paths);
|
|
493
|
-
const sourceDirectory = path.resolve(
|
|
538
|
+
const sourceDirectory = path.resolve(home);
|
|
494
539
|
if (!state.initialized || state.sourceDirectory !== sourceDirectory) {
|
|
495
|
-
throw new Error(
|
|
540
|
+
throw new Error(`Connect the ${source} source before importing its history.`);
|
|
496
541
|
}
|
|
497
|
-
const files = await
|
|
542
|
+
const files = await sourceFiles(source, sourceDirectory);
|
|
498
543
|
const cursors = {};
|
|
499
544
|
let bytes = 0;
|
|
500
545
|
for (const filename of files) {
|
|
501
546
|
cursors[filename] = 0;
|
|
502
547
|
bytes += (await fs.stat(filename)).size;
|
|
503
548
|
if (!Number.isSafeInteger(bytes)) {
|
|
504
|
-
throw new Error(
|
|
549
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
505
550
|
}
|
|
506
551
|
}
|
|
507
552
|
const localHealth = await healthSnapshot(paths);
|
|
@@ -517,13 +562,13 @@ export async function resetHistoryCursors(codexHome, paths) {
|
|
|
517
562
|
}, paths);
|
|
518
563
|
return { files: files.length, bytes };
|
|
519
564
|
}
|
|
520
|
-
export async function historyImportSize(
|
|
521
|
-
const files = await
|
|
565
|
+
export async function historyImportSize(home, source = "codex") {
|
|
566
|
+
const files = await sourceFiles(source, home);
|
|
522
567
|
let bytes = 0;
|
|
523
568
|
for (const filename of files) {
|
|
524
569
|
bytes += (await fs.stat(filename)).size;
|
|
525
570
|
if (!Number.isSafeInteger(bytes)) {
|
|
526
|
-
throw new Error(
|
|
571
|
+
throw new Error(`${source} history size exceeds the supported limit.`);
|
|
527
572
|
}
|
|
528
573
|
}
|
|
529
574
|
return { files: files.length, bytes };
|
|
@@ -557,7 +602,7 @@ async function lastCompleteRecordOffset(filename, size) {
|
|
|
557
602
|
await handle.close();
|
|
558
603
|
}
|
|
559
604
|
}
|
|
560
|
-
|
|
605
|
+
async function rolloutFiles(root) {
|
|
561
606
|
const files = [];
|
|
562
607
|
const pending = [root];
|
|
563
608
|
while (pending.length > 0) {
|
|
@@ -587,3 +632,51 @@ export async function rolloutFiles(root) {
|
|
|
587
632
|
}
|
|
588
633
|
return files.sort();
|
|
589
634
|
}
|
|
635
|
+
async function directoryEntries(directory) {
|
|
636
|
+
try {
|
|
637
|
+
return await fs.readdir(directory, { withFileTypes: true });
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
641
|
+
return [];
|
|
642
|
+
throw new Error(`Could not scan Claude session directory ${directory}.`, {
|
|
643
|
+
cause: error,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
export async function claudeFiles(root) {
|
|
648
|
+
const files = [];
|
|
649
|
+
for (const project of await directoryEntries(root)) {
|
|
650
|
+
if (!project.isDirectory())
|
|
651
|
+
continue;
|
|
652
|
+
const projectDirectory = path.join(root, project.name);
|
|
653
|
+
for (const entry of await directoryEntries(projectDirectory)) {
|
|
654
|
+
const filename = path.join(projectDirectory, entry.name);
|
|
655
|
+
if (entry.isFile() &&
|
|
656
|
+
/^[^/]+\.jsonl$/.test(entry.name) &&
|
|
657
|
+
!entry.name.includes(".orphaned-") &&
|
|
658
|
+
!entry.name.includes(".superseded-")) {
|
|
659
|
+
files.push(filename);
|
|
660
|
+
}
|
|
661
|
+
else if (entry.isDirectory()) {
|
|
662
|
+
const subagents = path.join(filename, "subagents");
|
|
663
|
+
for (const agent of await directoryEntries(subagents)) {
|
|
664
|
+
if (agent.isFile() && /^agent-.+\.jsonl$/.test(agent.name))
|
|
665
|
+
files.push(path.join(subagents, agent.name));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (files.length > 10_000)
|
|
669
|
+
throw new Error("Claude session file count exceeds the scan limit.");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return files.sort();
|
|
673
|
+
}
|
|
674
|
+
export function sourceFiles(source, home) {
|
|
675
|
+
const directory = path.resolve(home);
|
|
676
|
+
if (source === "codex")
|
|
677
|
+
return rolloutFiles(path.join(directory, "sessions"));
|
|
678
|
+
if (source === "claude")
|
|
679
|
+
return claudeFiles(path.join(directory, "projects"));
|
|
680
|
+
const unhandled = source;
|
|
681
|
+
throw new Error(`Unsupported collector source: ${unhandled}`);
|
|
682
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CollectorFrictionSummary } from "iteration-trace-contracts";
|
|
2
|
+
import { type CollectorConfig } from "./storage.js";
|
|
3
|
+
export type TurnSummaryPaths = Readonly<{
|
|
4
|
+
directory: string;
|
|
5
|
+
script: string;
|
|
6
|
+
requests: string;
|
|
7
|
+
lines: string;
|
|
8
|
+
shown: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function turnSummaryPaths(dataDirectory: string): TurnSummaryPaths;
|
|
11
|
+
export declare function installTurnSummary(config: CollectorConfig): Promise<void>;
|
|
12
|
+
export declare function uninstallTurnSummary(config: CollectorConfig): Promise<void>;
|
|
13
|
+
export declare function turnSummaryMessage(summary: CollectorFrictionSummary): string | null;
|
|
14
|
+
export declare class TurnSummaryRefreshError extends Error {
|
|
15
|
+
constructor(message: string, options?: ErrorOptions);
|
|
16
|
+
}
|
|
17
|
+
export declare function refreshTurnSummaries(config: CollectorConfig, refreshedAt: Map<string, Readonly<{
|
|
18
|
+
directory: string;
|
|
19
|
+
atMs: number;
|
|
20
|
+
}>>, nowMs: number, fetcher?: typeof fetch, signal?: AbortSignal): Promise<number>;
|
|
21
|
+
export declare function turnSummaryApprovalHint(config: CollectorConfig): string;
|