iteration-cli 0.7.0 → 0.7.2
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/auto-connect.d.ts +2 -1
- package/dist/auto-connect.js +3 -2
- package/dist/collector-fetch.d.ts +9 -0
- package/dist/collector-fetch.js +58 -0
- package/dist/collector.d.ts +2 -1
- package/dist/collector.js +15 -5
- package/dist/service.js +23 -2
- package/dist/turn-summary.d.ts +2 -1
- package/dist/turn-summary.js +2 -1
- package/package.json +1 -1
package/dist/auto-connect.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CollectorSource } from "iteration-trace-contracts";
|
|
2
|
+
import { type CollectorFetcher } from "./collector-fetch.js";
|
|
2
3
|
import { type CollectorConfig, type CollectorState } from "./storage.js";
|
|
3
4
|
export type MachineConsent = Readonly<{
|
|
4
5
|
version: 1;
|
|
@@ -24,7 +25,7 @@ export declare function readCollectorScope(anchor: CollectorConfig): Promise<Rea
|
|
|
24
25
|
organizationId: string;
|
|
25
26
|
organizationName: string;
|
|
26
27
|
}>>;
|
|
27
|
-
export declare function expandSource(anchor: CollectorConfig, source: CollectorSource, organizationId: string | null, request?:
|
|
28
|
+
export declare function expandSource(anchor: CollectorConfig, source: CollectorSource, organizationId: string | null, request?: CollectorFetcher): Promise<SourceEnrollment>;
|
|
28
29
|
export declare function activateSource(source: CollectorSource, home: string, endpoint: string, enrollment: SourceEnrollment, state: CollectorState): Promise<CollectorConfig>;
|
|
29
30
|
export declare function connectNewDetectedSources(anchor: CollectorConfig, consent: MachineConsent): Promise<CollectorSource[]>;
|
|
30
31
|
export declare function connectedConfig(source: CollectorSource): Promise<CollectorConfig | null>;
|
package/dist/auto-connect.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { COLLECTOR_SOURCES, parseCollectorEnrollmentResponse, } from "iteration-trace-contracts";
|
|
7
7
|
import { installBackgroundService } from "./background-service.js";
|
|
8
|
+
import { collectorFetch } from "./collector-fetch.js";
|
|
8
9
|
import { installTurnSummary } from "./turn-summary.js";
|
|
9
10
|
import { assertFreshCollectorDirectory, collectorPaths, defaultCollectorDirectory, ensurePrivateDirectory, loadConfig, prepareSourceInitialState, saveConfig, saveState, validateCollectorEndpoint, } from "./storage.js";
|
|
10
11
|
const consentSchema = z
|
|
@@ -92,7 +93,7 @@ export async function saveMachineConsent(consent) {
|
|
|
92
93
|
}
|
|
93
94
|
export async function readCollectorScope(anchor) {
|
|
94
95
|
const endpoint = validateCollectorEndpoint(anchor.endpoint);
|
|
95
|
-
const response = await
|
|
96
|
+
const response = await collectorFetch(new URL("./collector-auth/expand", endpoint), {
|
|
96
97
|
headers: { Authorization: `Bearer ${anchor.token}` },
|
|
97
98
|
signal: AbortSignal.timeout(10_000),
|
|
98
99
|
});
|
|
@@ -109,7 +110,7 @@ export async function readCollectorScope(anchor) {
|
|
|
109
110
|
throw new Error("Iteration returned an invalid collector workspace.");
|
|
110
111
|
return parsed.data;
|
|
111
112
|
}
|
|
112
|
-
export async function expandSource(anchor, source, organizationId, request =
|
|
113
|
+
export async function expandSource(anchor, source, organizationId, request = collectorFetch) {
|
|
113
114
|
const endpoint = validateCollectorEndpoint(anchor.endpoint);
|
|
114
115
|
const token = createHmac("sha256", anchor.token)
|
|
115
116
|
.update(`iteration-source:${source}`, "utf8")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type CollectorRequest = Readonly<{
|
|
2
|
+
method?: "GET" | "POST";
|
|
3
|
+
headers?: Readonly<Record<string, string>>;
|
|
4
|
+
body?: string;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}>;
|
|
7
|
+
export type CollectorFetcher = (url: string | URL, init?: CollectorRequest) => Promise<Response>;
|
|
8
|
+
export declare const collectorFetch: CollectorFetcher;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
4
|
+
const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
5
|
+
// Node's fetch can negotiate HTTP/2 and, after a timed-out request breaks the
|
|
6
|
+
// TLS connection, keep handing every later request the destroyed session until
|
|
7
|
+
// the process restarts. The collector runs for days, so it uses HTTP/1.1
|
|
8
|
+
// connections that are discarded when they fail.
|
|
9
|
+
const agents = {
|
|
10
|
+
"http:": new http.Agent({ keepAlive: true, maxSockets: 2 }),
|
|
11
|
+
"https:": new https.Agent({ keepAlive: true, maxSockets: 2 }),
|
|
12
|
+
};
|
|
13
|
+
export const collectorFetch = (input, init = {}) => {
|
|
14
|
+
const url = new URL(input);
|
|
15
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
16
|
+
throw new TypeError(`Unsupported collector request protocol ${url.protocol}`);
|
|
17
|
+
}
|
|
18
|
+
const body = init.body ?? null;
|
|
19
|
+
const headers = { ...init.headers };
|
|
20
|
+
if (body !== null) {
|
|
21
|
+
headers["content-length"] = String(Buffer.byteLength(body, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const request = transport.request(url, {
|
|
26
|
+
method: init.method ?? "GET",
|
|
27
|
+
headers,
|
|
28
|
+
agent: agents[url.protocol],
|
|
29
|
+
signal: init.signal,
|
|
30
|
+
}, (response) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
response.on("data", (chunk) => {
|
|
34
|
+
size += chunk.byteLength;
|
|
35
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
36
|
+
response.destroy(new Error("Iteration returned an oversized response."));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
});
|
|
41
|
+
response.on("error", reject);
|
|
42
|
+
response.on("end", () => {
|
|
43
|
+
const status = response.statusCode ?? 0;
|
|
44
|
+
const responseHeaders = new Headers();
|
|
45
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
49
|
+
responseHeaders.append(name, item);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolve(new Response(NULL_BODY_STATUSES.has(status) ? null : Buffer.concat(chunks), { status, headers: responseHeaders }));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
request.on("error", reject);
|
|
56
|
+
request.end(body ?? undefined);
|
|
57
|
+
});
|
|
58
|
+
};
|
package/dist/collector.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CollectorFetcher } from "./collector-fetch.js";
|
|
1
2
|
import { type CollectorConfig, type CollectorPaths } from "./storage.js";
|
|
2
3
|
export declare class TraceUploadError extends Error {
|
|
3
4
|
readonly kind: "http" | "network" | "response";
|
|
@@ -8,7 +9,7 @@ export declare function scanSourceOnce(config: CollectorConfig, paths: Collector
|
|
|
8
9
|
discovered: number;
|
|
9
10
|
caughtUp: boolean;
|
|
10
11
|
}>>;
|
|
11
|
-
export declare function collectorTick(config: CollectorConfig, paths: CollectorPaths, fetcher?:
|
|
12
|
+
export declare function collectorTick(config: CollectorConfig, paths: CollectorPaths, fetcher?: CollectorFetcher, signal?: AbortSignal, onAcknowledged?: () => void): Promise<Readonly<{
|
|
12
13
|
discovered: number;
|
|
13
14
|
uploaded: number;
|
|
14
15
|
outboxBytes: number;
|
package/dist/collector.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseTraceEventAcknowledgement, } from "iteration-trace-contracts";
|
|
4
|
+
import { collectorFetch } from "./collector-fetch.js";
|
|
4
5
|
import { parseClaudeLine } from "./claude-transcript.js";
|
|
5
6
|
import { parseRolloutLine } from "./rollout.js";
|
|
6
7
|
import { acknowledgeEvents, appendEvents, collectorSource, collectorSourceDirectory, loadState, nextBatch, outboxSize, pendingEventIds, sourceFiles, saveState, } from "./storage.js";
|
|
@@ -11,6 +12,13 @@ const MAX_UPLOAD_BYTES = 3 * 1024 * 1024;
|
|
|
11
12
|
const MAX_EVENT_BYTES = MAX_UPLOAD_BYTES - 1024;
|
|
12
13
|
const MAX_UPLOADS_PER_TICK = 5;
|
|
13
14
|
const MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
// Production stores some batches in about 40 seconds. Abandoning a request
|
|
16
|
+
// earlier does not stop the server, so the retry only waits behind it and the
|
|
17
|
+
// collector reports a failure for work that succeeded.
|
|
18
|
+
const UPLOAD_TIMEOUT_MS = 90_000;
|
|
19
|
+
// A tick can queue up to MAX_SCAN_BYTES_PER_TICK of new events, so scanning
|
|
20
|
+
// pauses well below the outbox capacity and resumes once uploads drain it.
|
|
21
|
+
const SCAN_PAUSE_OUTBOX_BYTES = 32 * 1024 * 1024;
|
|
14
22
|
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
15
23
|
export class TraceUploadError extends Error {
|
|
16
24
|
kind;
|
|
@@ -157,7 +165,7 @@ export async function scanSourceOnce(config, paths, signal) {
|
|
|
157
165
|
}, paths);
|
|
158
166
|
return { discovered: events.length, caughtUp };
|
|
159
167
|
}
|
|
160
|
-
async function uploadPendingOnce(config, paths, fetcher =
|
|
168
|
+
async function uploadPendingOnce(config, paths, fetcher = collectorFetch, signal, onAcknowledged) {
|
|
161
169
|
let uploaded = 0;
|
|
162
170
|
for (let attempt = 0; attempt < MAX_UPLOADS_PER_TICK; attempt += 1) {
|
|
163
171
|
signal?.throwIfAborted();
|
|
@@ -178,8 +186,8 @@ async function uploadPendingOnce(config, paths, fetcher = fetch, signal, onAckno
|
|
|
178
186
|
},
|
|
179
187
|
body,
|
|
180
188
|
signal: signal
|
|
181
|
-
? AbortSignal.any([signal, AbortSignal.timeout(
|
|
182
|
-
: AbortSignal.timeout(
|
|
189
|
+
? AbortSignal.any([signal, AbortSignal.timeout(UPLOAD_TIMEOUT_MS)])
|
|
190
|
+
: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
|
|
183
191
|
});
|
|
184
192
|
}
|
|
185
193
|
catch (error) {
|
|
@@ -208,9 +216,11 @@ async function uploadPendingOnce(config, paths, fetcher = fetch, signal, onAckno
|
|
|
208
216
|
}
|
|
209
217
|
return uploaded;
|
|
210
218
|
}
|
|
211
|
-
export async function collectorTick(config, paths, fetcher =
|
|
219
|
+
export async function collectorTick(config, paths, fetcher = collectorFetch, signal, onAcknowledged) {
|
|
212
220
|
let uploaded = await uploadPendingOnce(config, paths, fetcher, signal, onAcknowledged);
|
|
213
|
-
const scan = await
|
|
221
|
+
const scan = (await outboxSize(paths)) >= SCAN_PAUSE_OUTBOX_BYTES
|
|
222
|
+
? { discovered: 0, caughtUp: false }
|
|
223
|
+
: await scanSourceOnce(config, paths, signal);
|
|
214
224
|
uploaded += await uploadPendingOnce(config, paths, fetcher, signal, onAcknowledged);
|
|
215
225
|
return {
|
|
216
226
|
discovered: scan.discovered,
|
package/dist/service.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import { connectNewDetectedSources, loadMachineConsent, } from "./auto-connect.js";
|
|
5
|
+
import { collectorFetch } from "./collector-fetch.js";
|
|
5
6
|
import { collectorTick, TraceUploadError } from "./collector.js";
|
|
6
7
|
import { refreshTurnSummaries } from "./turn-summary.js";
|
|
7
8
|
import { collectorSource, collectorPaths, ensureClaudeHistoryBackfill, ensureHistoryBackfill, ensurePrivateDirectory, healthSnapshot, loadConfig, saveHealth, } from "./storage.js";
|
|
@@ -12,6 +13,21 @@ function errorCode(error) {
|
|
|
12
13
|
}
|
|
13
14
|
class CollectorLockBusyError extends Error {
|
|
14
15
|
}
|
|
16
|
+
// Fetch reports network failures as a generic TypeError; the actionable
|
|
17
|
+
// reason (DNS, TLS, reset, timeout) is only in its cause chain.
|
|
18
|
+
function errorCauses(error) {
|
|
19
|
+
const causes = [];
|
|
20
|
+
let current = error instanceof Error ? error.cause : undefined;
|
|
21
|
+
while (current instanceof Error && causes.length < 4) {
|
|
22
|
+
causes.push({
|
|
23
|
+
name: current.name,
|
|
24
|
+
code: errorCode(current),
|
|
25
|
+
message: current.message.slice(0, 300),
|
|
26
|
+
});
|
|
27
|
+
current = current.cause;
|
|
28
|
+
}
|
|
29
|
+
return causes;
|
|
30
|
+
}
|
|
15
31
|
function collectorFailure(error) {
|
|
16
32
|
return {
|
|
17
33
|
atMs: Date.now(),
|
|
@@ -143,7 +159,7 @@ export async function runCollector(directory, expectedSource) {
|
|
|
143
159
|
}
|
|
144
160
|
let acknowledgedAtMs = null;
|
|
145
161
|
try {
|
|
146
|
-
const result = await collectorTick(config, paths,
|
|
162
|
+
const result = await collectorTick(config, paths, collectorFetch, controller.signal, () => {
|
|
147
163
|
acknowledgedAtMs = Date.now();
|
|
148
164
|
});
|
|
149
165
|
if (acknowledgedAtMs !== null ||
|
|
@@ -160,6 +176,7 @@ export async function runCollector(directory, expectedSource) {
|
|
|
160
176
|
if (result.discovered > 0 || result.uploaded > 0) {
|
|
161
177
|
console.info(JSON.stringify({
|
|
162
178
|
level: "info",
|
|
179
|
+
at: new Date().toISOString(),
|
|
163
180
|
source,
|
|
164
181
|
discovered: result.discovered,
|
|
165
182
|
uploaded: result.uploaded,
|
|
@@ -178,9 +195,11 @@ export async function runCollector(directory, expectedSource) {
|
|
|
178
195
|
};
|
|
179
196
|
console.error(JSON.stringify({
|
|
180
197
|
level: "error",
|
|
198
|
+
at: new Date().toISOString(),
|
|
181
199
|
source,
|
|
182
200
|
operation: "collector_tick",
|
|
183
201
|
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
202
|
+
causes: errorCauses(error),
|
|
184
203
|
message: error instanceof Error
|
|
185
204
|
? error.message
|
|
186
205
|
: "Unknown collector failure.",
|
|
@@ -189,16 +208,18 @@ export async function runCollector(directory, expectedSource) {
|
|
|
189
208
|
retryDelayMs = Math.min(retryDelayMs * 2, 60_000);
|
|
190
209
|
}
|
|
191
210
|
try {
|
|
192
|
-
await refreshTurnSummaries(config, summariesRefreshedAt, Date.now(),
|
|
211
|
+
await refreshTurnSummaries(config, summariesRefreshedAt, Date.now(), collectorFetch, controller.signal);
|
|
193
212
|
}
|
|
194
213
|
catch (error) {
|
|
195
214
|
if (controller.signal.aborted)
|
|
196
215
|
break;
|
|
197
216
|
console.error(JSON.stringify({
|
|
198
217
|
level: "warning",
|
|
218
|
+
at: new Date().toISOString(),
|
|
199
219
|
source,
|
|
200
220
|
operation: "refresh_turn_summaries",
|
|
201
221
|
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
222
|
+
causes: errorCauses(error),
|
|
202
223
|
message: error instanceof Error
|
|
203
224
|
? error.message
|
|
204
225
|
: "Unknown turn summary failure.",
|
package/dist/turn-summary.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CollectorFrictionSummary } from "iteration-trace-contracts";
|
|
2
|
+
import { type CollectorFetcher } from "./collector-fetch.js";
|
|
2
3
|
import { type CollectorConfig } from "./storage.js";
|
|
3
4
|
export type TurnSummaryPaths = Readonly<{
|
|
4
5
|
directory: string;
|
|
@@ -17,6 +18,6 @@ export declare class TurnSummaryRefreshError extends Error {
|
|
|
17
18
|
export declare function refreshTurnSummaries(config: CollectorConfig, refreshedAt: Map<string, Readonly<{
|
|
18
19
|
directory: string;
|
|
19
20
|
atMs: number;
|
|
20
|
-
}>>, nowMs: number, fetcher?:
|
|
21
|
+
}>>, nowMs: number, fetcher?: CollectorFetcher, signal?: AbortSignal): Promise<number>;
|
|
21
22
|
export type TurnSummaryApproval = "approved" | "needs_approval" | "not_installed";
|
|
22
23
|
export declare function codexTurnSummaryApproval(config: CollectorConfig): Promise<TurnSummaryApproval>;
|
package/dist/turn-summary.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isLaunchDirectory, parseCollectorFrictionSummary, } from "iteration-trace-contracts";
|
|
5
5
|
import * as z from "zod";
|
|
6
|
+
import { collectorFetch } from "./collector-fetch.js";
|
|
6
7
|
import { ensurePrivateDirectory } from "./storage.js";
|
|
7
8
|
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
8
9
|
const REQUEST_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
@@ -249,7 +250,7 @@ async function forget(paths, key) {
|
|
|
249
250
|
// Refreshes the summary line for each repository the hook asked about. The
|
|
250
251
|
// hook reads the result on a later turn, so a slow or failed request never
|
|
251
252
|
// delays the agent.
|
|
252
|
-
export async function refreshTurnSummaries(config, refreshedAt, nowMs, fetcher =
|
|
253
|
+
export async function refreshTurnSummaries(config, refreshedAt, nowMs, fetcher = collectorFetch, signal) {
|
|
253
254
|
const paths = turnSummaryPaths(config.dataDirectory);
|
|
254
255
|
let keys;
|
|
255
256
|
try {
|